musikr: initial root documentation

This commit is contained in:
Alexander Capehart 2025-01-14 08:53:27 -07:00
parent b6d80189ca
commit c9d4b01f9f
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 181 additions and 7 deletions

View file

@ -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
)

View file

@ -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<Song>
val albums: Collection<Album>
@ -27,31 +32,131 @@ interface Library {
val genres: Collection<Genre>
val playlists: Collection<Playlist>
/**
* 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<Song>): 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<Song>): 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<Song>): 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
}

View file

@ -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<MusicLocation>,
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 })
}