From c9d4b01f9f70e88a20a133e7378823f5d0dfc495 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 14 Jan 2025 08:53:27 -0700 Subject: [PATCH] musikr: initial root documentation --- .../main/java/org/oxycblt/musikr/Config.kt | 26 ++++- .../main/java/org/oxycblt/musikr/Library.kt | 105 ++++++++++++++++++ .../main/java/org/oxycblt/musikr/Musikr.kt | 57 +++++++++- 3 files changed, 181 insertions(+), 7 deletions(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt index cdd5a3f56..a793680db 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -24,10 +24,34 @@ import org.oxycblt.musikr.playlist.db.StoredPlaylists import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Separators +/** Side-effect laden [Storage] for use during music loading and [MutableLibrary] operation. */ data class Storage( + /** + * A factory producing a repository of cached metadata to read and write from over the course of + * music loading. This will only be used during music loading. + */ val cache: Cache.Factory, + + /** + * A repository of cover images to for re-use during music loading. Should be kept in lock-step + * with the cache for best performance. This will be used during music loading and when + * retrieving cover information from the library. + */ val storedCovers: MutableCovers, + + /** + * A repository of user-created playlists that should also be loaded into the library. This will + * be used during music loading and mutated when creating, renaming, or deleting playlists in + * the library. + */ val storedPlaylists: StoredPlaylists ) -data class Interpretation(val naming: Naming, val separators: Separators) +/** Configuration for how to interpret and extrapolate certain audio tags. */ +data class Interpretation( + /** How to construct names from audio tags. */ + val naming: Naming, + + /** What separators delimit multi-value audio tags. */ + val separators: Separators +) diff --git a/musikr/src/main/java/org/oxycblt/musikr/Library.kt b/musikr/src/main/java/org/oxycblt/musikr/Library.kt index 59af89894..fea071da0 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Library.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Library.kt @@ -20,6 +20,11 @@ package org.oxycblt.musikr import org.oxycblt.musikr.fs.Path +/** + * An immutable music library. + * + * No operations here will create side effects. + */ interface Library { val songs: Collection val albums: Collection @@ -27,31 +32,131 @@ interface Library { val genres: Collection val playlists: Collection + /** + * Whether this library is empty (i.e no songs, which means no other music item) + * + * @return true if this library is empty, false otherwise + */ fun empty(): Boolean + /** + * Find a [Song] by it's [Music.UID] + * + * @param uid the [Music.UID] of the song + * @return the song if found, null otherwise + */ fun findSong(uid: Music.UID): Song? + /** + * Find a [Song] by it's [Path] + * + * @param path the [Path] of the song + * @return the song if found, null otherwise + */ fun findSongByPath(path: Path): Song? + /** + * Find an [Album] by it's [Music.UID] + * + * @param uid the [Music.UID] of the album + * @return the album if found, null otherwise + */ fun findAlbum(uid: Music.UID): Album? + /** + * Find an [Artist] by it's [Music.UID] + * + * @param uid the [Music.UID] of the artist + * @return the artist if found, null otherwise + */ fun findArtist(uid: Music.UID): Artist? + /** + * Find a [Genre] by it's [Music.UID] + * + * @param uid the [Music.UID] of the genre + * @return the genre if found, null otherwise + */ fun findGenre(uid: Music.UID): Genre? + /** + * Find a [Playlist] by it's [Music.UID] + * + * @param uid the [Music.UID] of the playlist + * @return the playlist if found, null otherwise + */ fun findPlaylist(uid: Music.UID): Playlist? + /** + * Find a [Playlist] by it's name + * + * @param name the name of the playlist + * @return the playlist if found, null otherwise + */ fun findPlaylistByName(name: String): Playlist? } +/** + * A mutable extension of [Library]. + * + * Operations here will cause side-effects within the [Storage] used when this library was loaded. + * However, it won't actually mutate the [Library] itself, rather return a cloned instance with the + * changes applied. It is up to the client to update their reference to the library within their + * state handling. + */ interface MutableLibrary : Library { + /** + * Create a new [Playlist] with the given name and songs. + * + * This will commit the new playlist to the stored playlists in the [Storage] used to load the + * library. + * + * @param name the name of the playlist + * @param songs the songs to add to the playlist + * @return a new [MutableLibrary] with the new playlist + */ suspend fun createPlaylist(name: String, songs: List): MutableLibrary + /** + * Rename a [Playlist]. + * + * This will commit to whatever playlist source the given [Playlist] was loaded from. + * + * @param playlist the playlist to rename + * @param name the new name of the playlist + * @return a new [MutableLibrary] with the renamed playlist + */ suspend fun renamePlaylist(playlist: Playlist, name: String): MutableLibrary + /** + * Add songs to a [Playlist]. + * + * This will commit to whatever playlist source the given [Playlist] was loaded from. + * + * @param playlist the playlist to add songs to + * @param songs the songs to add to the playlist + * @return a new [MutableLibrary] with the edited playlist + */ suspend fun addToPlaylist(playlist: Playlist, songs: List): MutableLibrary + /** + * Remove songs from a [Playlist]. + * + * This will commit to whatever playlist source the given [Playlist] was loaded from. + * + * @param playlist the playlist to remove songs from + * @param songs the songs to remove from the playlist + * @return a new [MutableLibrary] with the edited playlist + */ suspend fun rewritePlaylist(playlist: Playlist, songs: List): MutableLibrary + /** + * Remove a [Playlist]. + * + * This will commit to whatever playlist source the given [Playlist] was loaded from. + * + * @param playlist the playlist to delete + * @return a new [MutableLibrary] with the edited playlist + */ suspend fun deletePlaylist(playlist: Playlist): MutableLibrary } diff --git a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt index 3f061c5f5..c18a01684 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Musikr.kt @@ -30,13 +30,44 @@ import org.oxycblt.musikr.pipeline.EvaluateStep import org.oxycblt.musikr.pipeline.ExploreStep import org.oxycblt.musikr.pipeline.ExtractStep +/** + * A highly opinionated, multi-threaded device music library. + * + * Use this to load music with [run]. + * + * Note the following: + * 1. Musikr's API surface is intended to be primarily "stateless", with side-effects mostly + * contained within [Storage]. It's your job to manage long-term state. + * 2. There are no "defaults" in Musikr. You should think carefully about the parameters you are + * specifying and know consider they are desirable or not. + * 3. Musikr is currently not extendable, so if you're embedding this elsewhere you should be ready + * to fork and modify the source code. + */ interface Musikr { + /** + * Start loading music from the given [locations] and the configuration provided earlier. + * + * @param locations The [MusicLocation]s to search for music in. + * @param onProgress Optional callback to receive progress on the current status of the music + * pipeline. Warning: These events will be rapid-fire. + * @return A handle to the newly created library alongside further cleanup. + */ suspend fun run( locations: List, onProgress: suspend (IndexingProgress) -> Unit = {} ): LibraryResult companion object { + /** + * Create a new instance from the given configuration. + * + * @param context The context to use for loading resources. + * @param storage Side-effect laden storage for use within the music loader **and** when + * mutating [MutableLibrary]. You should take responsibility for managing their long-term + * state. + * @param interpretation The configuration to use for interpreting certain vague tags. This + * should be configured by the user, if possible. + */ fun new(context: Context, storage: Storage, interpretation: Interpretation): Musikr = MusikrImpl( storage, @@ -46,20 +77,35 @@ interface Musikr { } } +/** Simple library handle returned by [Musikr.run]. */ interface LibraryResult { val library: MutableLibrary + /** + * Clean up expired resources. This should be done as soon as possible after music loading to + * reduce storage use. + * + * This may have unexpected results if previous [Library]s are in circulation across your app, + * so use it once you've fully updated your state. + */ suspend fun cleanup() } -/** - * Represents the current progress of music loading. - * - * @author Alexander Capehart (OxygenCobalt) - */ +/** Music loading progress as reported by the music pipeline. */ sealed interface IndexingProgress { + /** + * Currently indexing and extracting tags from device music. + * + * @param explored The amount of music currently found from the given [MusicLocation]s. + * @param loaded The amount of music that has had metadata extracted and parsed. + */ data class Songs(val loaded: Int, val explored: Int) : IndexingProgress + /** + * Currently creating the music graph alongside I/O finalization. + * + * There is no way to measure progress on these events. + */ data object Indeterminate : IndexingProgress } @@ -96,7 +142,6 @@ private class LibraryResultImpl( private val storage: Storage, override val library: MutableLibrary ) : LibraryResult { - override suspend fun cleanup() { storage.storedCovers.cleanup(library.songs.mapNotNull { it.cover }) }