musikr: move storage/interpretation dependence to construction

This makes some testing and certain code more ergonomic.
This commit is contained in:
Alexander Capehart 2024-12-17 11:45:04 -05:00
parent f3913b148a
commit bdfd9d6e23
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 87 additions and 81 deletions

View file

@ -26,7 +26,6 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
import org.oxycblt.musikr.Musikr
import org.oxycblt.musikr.cache.CacheDatabase import org.oxycblt.musikr.cache.CacheDatabase
import org.oxycblt.musikr.playlist.db.PlaylistDatabase import org.oxycblt.musikr.playlist.db.PlaylistDatabase
@ -48,6 +47,4 @@ class MusikrShimModule {
@Singleton @Singleton
@Provides @Provides
fun playlistDatabase(@ApplicationContext context: Context) = PlaylistDatabase.from(context) fun playlistDatabase(@ApplicationContext context: Context) = PlaylistDatabase.from(context)
@Provides fun musikr(@ApplicationContext context: Context) = Musikr.new(context)
} }

View file

@ -58,6 +58,7 @@ import timber.log.Timber as L
*/ */
interface MusicRepository { interface MusicRepository {
val library: Library? val library: Library?
/** The current state of music loading. Null if no load has occurred yet. */ /** The current state of music loading. Null if no load has occurred yet. */
val indexingState: IndexingState? val indexingState: IndexingState?
@ -212,7 +213,6 @@ interface MusicRepository {
class MusicRepositoryImpl class MusicRepositoryImpl
@Inject @Inject
constructor( constructor(
private val musikr: Musikr,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val cacheDatabase: CacheDatabase, private val cacheDatabase: CacheDatabase,
private val playlistDatabase: PlaylistDatabase, private val playlistDatabase: PlaylistDatabase,
@ -375,9 +375,11 @@ constructor(
StoredCovers.from(context, "covers"), StoredCovers.from(context, "covers"),
StoredPlaylists.from(playlistDatabase)) StoredPlaylists.from(playlistDatabase))
} }
val interpretation = Interpretation(nameFactory, separators)
val newLibrary = val newLibrary =
musikr.run( Musikr.new(context, storage, interpretation).run(locations, ::emitIndexingProgress)
locations, storage, Interpretation(nameFactory, separators), ::emitIndexingProgress)
emitIndexingCompletion(null) emitIndexingCompletion(null)

View file

@ -18,7 +18,6 @@
package org.oxycblt.musikr package org.oxycblt.musikr
import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path
interface Library { interface Library {
@ -28,8 +27,6 @@ interface Library {
val genres: Collection<Genre> val genres: Collection<Genre>
val playlists: Collection<Playlist> val playlists: Collection<Playlist>
val storedCovers: StoredCovers
fun findSong(uid: Music.UID): Song? fun findSong(uid: Music.UID): Song?
fun findSongByPath(path: Path): Song? fun findSongByPath(path: Path): Song?

View file

@ -33,14 +33,15 @@ import org.oxycblt.musikr.pipeline.ExtractStep
interface Musikr { interface Musikr {
suspend fun run( suspend fun run(
locations: List<MusicLocation>, locations: List<MusicLocation>,
storage: Storage,
interpretation: Interpretation,
onProgress: suspend (IndexingProgress) -> Unit = {} onProgress: suspend (IndexingProgress) -> Unit = {}
): MutableLibrary ): MutableLibrary
companion object { companion object {
fun new(context: Context): Musikr = fun new(context: Context, storage: Storage, interpretation: Interpretation): Musikr =
MusikrImpl(ExploreStep.from(context), ExtractStep.from(context), EvaluateStep.new()) MusikrImpl(
ExploreStep.from(context, storage),
ExtractStep.from(context, storage),
EvaluateStep.new(storage, interpretation))
} }
} }
@ -62,24 +63,22 @@ private class MusikrImpl(
) : Musikr { ) : Musikr {
override suspend fun run( override suspend fun run(
locations: List<MusicLocation>, locations: List<MusicLocation>,
storage: Storage,
interpretation: Interpretation,
onProgress: suspend (IndexingProgress) -> Unit onProgress: suspend (IndexingProgress) -> Unit
) = coroutineScope { ) = coroutineScope {
var exploredCount = 0 var exploredCount = 0
var extractedCount = 0 var extractedCount = 0
val explored = val explored =
exploreStep exploreStep
.explore(locations, storage) .explore(locations)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
.onStart { onProgress(IndexingProgress.Songs(0, 0)) } .onStart { onProgress(IndexingProgress.Songs(0, 0)) }
.onEach { onProgress(IndexingProgress.Songs(extractedCount, ++exploredCount)) } .onEach { onProgress(IndexingProgress.Songs(extractedCount, ++exploredCount)) }
val extracted = val extracted =
extractStep extractStep
.extract(storage, explored) .extract(explored)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
.onEach { onProgress(IndexingProgress.Songs(++extractedCount, exploredCount)) } .onEach { onProgress(IndexingProgress.Songs(++extractedCount, exploredCount)) }
.onCompletion { onProgress(IndexingProgress.Indeterminate) } .onCompletion { onProgress(IndexingProgress.Indeterminate) }
evaluateStep.evaluate(storage, interpretation, extracted) evaluateStep.evaluate(extracted)
} }
} }

View file

@ -21,19 +21,23 @@ package org.oxycblt.musikr.model
import org.oxycblt.musikr.Album import org.oxycblt.musikr.Album
import org.oxycblt.musikr.Artist import org.oxycblt.musikr.Artist
import org.oxycblt.musikr.Genre import org.oxycblt.musikr.Genre
import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.MutableLibrary import org.oxycblt.musikr.MutableLibrary
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.graph.AlbumVertex import org.oxycblt.musikr.graph.AlbumVertex
import org.oxycblt.musikr.graph.ArtistVertex import org.oxycblt.musikr.graph.ArtistVertex
import org.oxycblt.musikr.graph.GenreVertex import org.oxycblt.musikr.graph.GenreVertex
import org.oxycblt.musikr.graph.MusicGraph import org.oxycblt.musikr.graph.MusicGraph
import org.oxycblt.musikr.graph.PlaylistVertex import org.oxycblt.musikr.graph.PlaylistVertex
import org.oxycblt.musikr.graph.SongVertex import org.oxycblt.musikr.graph.SongVertex
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter
internal interface LibraryFactory { internal interface LibraryFactory {
fun create(graph: MusicGraph, storage: Storage, interpretation: Interpretation): MutableLibrary fun create(
graph: MusicGraph,
storedPlaylists: StoredPlaylists,
playlistInterpreter: PlaylistInterpreter
): MutableLibrary
companion object { companion object {
fun new(): LibraryFactory = LibraryFactoryImpl() fun new(): LibraryFactory = LibraryFactoryImpl()
@ -43,8 +47,8 @@ internal interface LibraryFactory {
private class LibraryFactoryImpl() : LibraryFactory { private class LibraryFactoryImpl() : LibraryFactory {
override fun create( override fun create(
graph: MusicGraph, graph: MusicGraph,
storage: Storage, storedPlaylists: StoredPlaylists,
interpretation: Interpretation playlistInterpreter: PlaylistInterpreter
): MutableLibrary { ): MutableLibrary {
val songs = val songs =
graph.songVertex.mapTo(mutableSetOf()) { vertex -> graph.songVertex.mapTo(mutableSetOf()) { vertex ->
@ -66,7 +70,8 @@ private class LibraryFactoryImpl() : LibraryFactory {
graph.playlistVertex.mapTo(mutableSetOf()) { vertex -> graph.playlistVertex.mapTo(mutableSetOf()) { vertex ->
PlaylistImpl(PlaylistVertexCore(vertex)) PlaylistImpl(PlaylistVertexCore(vertex))
} }
return LibraryImpl(songs, albums, artists, genres, playlists, storage, interpretation) return LibraryImpl(
songs, albums, artists, genres, playlists, storedPlaylists, playlistInterpreter)
} }
private class SongVertexCore(private val vertex: SongVertex) : SongCore { private class SongVertexCore(private val vertex: SongVertex) : SongCore {

View file

@ -18,13 +18,14 @@
package org.oxycblt.musikr.model package org.oxycblt.musikr.model
import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.Music import org.oxycblt.musikr.Music
import org.oxycblt.musikr.MutableLibrary import org.oxycblt.musikr.MutableLibrary
import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter
import org.oxycblt.musikr.playlist.interpret.PostPlaylist
internal data class LibraryImpl( internal data class LibraryImpl(
override val songs: Collection<SongImpl>, override val songs: Collection<SongImpl>,
@ -32,8 +33,8 @@ internal data class LibraryImpl(
override val artists: Collection<ArtistImpl>, override val artists: Collection<ArtistImpl>,
override val genres: Collection<GenreImpl>, override val genres: Collection<GenreImpl>,
override val playlists: Collection<Playlist>, override val playlists: Collection<Playlist>,
private val storage: Storage, private val storedPlaylists: StoredPlaylists,
private val interpretation: Interpretation private val playlistInterpreter: PlaylistInterpreter
) : MutableLibrary { ) : MutableLibrary {
private val songUidMap = songs.associateBy { it.uid } private val songUidMap = songs.associateBy { it.uid }
private val albumUidMap = albums.associateBy { it.uid } private val albumUidMap = albums.associateBy { it.uid }
@ -41,8 +42,6 @@ internal data class LibraryImpl(
private val genreUidMap = genres.associateBy { it.uid } private val genreUidMap = genres.associateBy { it.uid }
private val playlistUidMap = playlists.associateBy { it.uid } private val playlistUidMap = playlists.associateBy { it.uid }
override val storedCovers = storage.storedCovers
override fun findSong(uid: Music.UID) = songUidMap[uid] override fun findSong(uid: Music.UID) = songUidMap[uid]
override fun findSongByPath(path: Path) = songs.find { it.path == path } override fun findSongByPath(path: Path) = songs.find { it.path == path }
@ -76,4 +75,9 @@ internal data class LibraryImpl(
override suspend fun deletePlaylist(playlist: Playlist): MutableLibrary { override suspend fun deletePlaylist(playlist: Playlist): MutableLibrary {
return this return this
} }
private class NewPlaylistCore(
override val prePlaylist: PostPlaylist,
override val songs: List<Song>
) : PlaylistCore
} }

View file

@ -32,32 +32,30 @@ import org.oxycblt.musikr.MutableLibrary
import org.oxycblt.musikr.Storage import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.graph.MusicGraph import org.oxycblt.musikr.graph.MusicGraph
import org.oxycblt.musikr.model.LibraryFactory import org.oxycblt.musikr.model.LibraryFactory
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter
import org.oxycblt.musikr.tag.interpret.TagInterpreter import org.oxycblt.musikr.tag.interpret.TagInterpreter
internal interface EvaluateStep { internal interface EvaluateStep {
suspend fun evaluate( suspend fun evaluate(extractedMusic: Flow<ExtractedMusic>): MutableLibrary
storage: Storage,
interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic>
): MutableLibrary
companion object { companion object {
fun new(): EvaluateStep = fun new(storage: Storage, interpretation: Interpretation): EvaluateStep =
EvaluateStepImpl(TagInterpreter.new(), PlaylistInterpreter.new(), LibraryFactory.new()) EvaluateStepImpl(
TagInterpreter.new(interpretation),
PlaylistInterpreter.new(interpretation),
storage.storedPlaylists,
LibraryFactory.new())
} }
} }
private class EvaluateStepImpl( private class EvaluateStepImpl(
private val tagInterpreter: TagInterpreter, private val tagInterpreter: TagInterpreter,
private val playlistInterpreter: PlaylistInterpreter, private val playlistInterpreter: PlaylistInterpreter,
private val storedPlaylists: StoredPlaylists,
private val libraryFactory: LibraryFactory private val libraryFactory: LibraryFactory
) : EvaluateStep { ) : EvaluateStep {
override suspend fun evaluate( override suspend fun evaluate(extractedMusic: Flow<ExtractedMusic>): MutableLibrary {
storage: Storage,
interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic>
): MutableLibrary {
val filterFlow = val filterFlow =
extractedMusic.divert { extractedMusic.divert {
when (it) { when (it) {
@ -68,12 +66,12 @@ private class EvaluateStepImpl(
val rawSongs = filterFlow.right val rawSongs = filterFlow.right
val preSongs = val preSongs =
rawSongs rawSongs
.map { tagInterpreter.interpret(it, interpretation) } .map { tagInterpreter.interpret(it) }
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
val prePlaylists = val prePlaylists =
filterFlow.left filterFlow.left
.map { playlistInterpreter.interpret(it, interpretation) } .map { playlistInterpreter.interpret(it) }
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
val graphBuilder = MusicGraph.builder() val graphBuilder = MusicGraph.builder()
@ -84,6 +82,6 @@ private class EvaluateStepImpl(
prePlaylists.onEach { graphBuilder.add(it) }) prePlaylists.onEach { graphBuilder.add(it) })
graphBuild.collect() graphBuild.collect()
val graph = graphBuilder.build() val graph = graphBuilder.build()
return libraryFactory.create(graph, storage, interpretation) return libraryFactory.create(graph, storedPlaylists, playlistInterpreter)
} }
} }

View file

@ -34,18 +34,23 @@ import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.MusicLocation
import org.oxycblt.musikr.fs.query.DeviceFiles import org.oxycblt.musikr.fs.query.DeviceFiles
import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.m3u.M3U import org.oxycblt.musikr.playlist.m3u.M3U
internal interface ExploreStep { internal interface ExploreStep {
fun explore(locations: List<MusicLocation>, storage: Storage): Flow<ExploreNode> fun explore(locations: List<MusicLocation>): Flow<ExploreNode>
companion object { companion object {
fun from(context: Context): ExploreStep = ExploreStepImpl(DeviceFiles.from(context)) fun from(context: Context, storage: Storage): ExploreStep =
ExploreStepImpl(DeviceFiles.from(context), storage.storedPlaylists)
} }
} }
private class ExploreStepImpl(private val deviceFiles: DeviceFiles) : ExploreStep { private class ExploreStepImpl(
override fun explore(locations: List<MusicLocation>, storage: Storage): Flow<ExploreNode> { private val deviceFiles: DeviceFiles,
private val storedPlaylists: StoredPlaylists
) : ExploreStep {
override fun explore(locations: List<MusicLocation>): Flow<ExploreNode> {
val audios = val audios =
deviceFiles deviceFiles
.explore(locations.asFlow()) .explore(locations.asFlow())
@ -59,7 +64,7 @@ private class ExploreStepImpl(private val deviceFiles: DeviceFiles) : ExploreSte
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer() .buffer()
val playlists = val playlists =
flow { emitAll(storage.storedPlaylists.read().asFlow()) } flow { emitAll(storedPlaylists.read().asFlow()) }
.map { ExploreNode.Playlist(it) } .map { ExploreNode.Playlist(it) }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer() .buffer()

View file

@ -28,8 +28,10 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.Storage import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cache.CacheResult import org.oxycblt.musikr.cache.CacheResult
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.metadata.MetadataExtractor import org.oxycblt.musikr.metadata.MetadataExtractor
import org.oxycblt.musikr.metadata.Properties import org.oxycblt.musikr.metadata.Properties
@ -38,19 +40,25 @@ import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.parse.TagParser import org.oxycblt.musikr.tag.parse.TagParser
internal interface ExtractStep { internal interface ExtractStep {
fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic> fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic>
companion object { companion object {
fun from(context: Context): ExtractStep = fun from(context: Context, storage: Storage): ExtractStep =
ExtractStepImpl(MetadataExtractor.from(context), TagParser.new()) ExtractStepImpl(
MetadataExtractor.from(context),
TagParser.new(),
storage.cache,
storage.storedCovers)
} }
} }
private class ExtractStepImpl( private class ExtractStepImpl(
private val metadataExtractor: MetadataExtractor, private val metadataExtractor: MetadataExtractor,
private val tagParser: TagParser private val tagParser: TagParser,
private val cache: Cache,
private val storedCovers: StoredCovers
) : ExtractStep { ) : ExtractStep {
override fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic> { override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
val filterFlow = val filterFlow =
nodes.divert { nodes.divert {
when (it) { when (it) {
@ -62,10 +70,7 @@ private class ExtractStepImpl(
val playlistNodes = filterFlow.left.map { ExtractedMusic.Playlist(it) } val playlistNodes = filterFlow.left.map { ExtractedMusic.Playlist(it) }
val cacheResults = val cacheResults =
audioNodes audioNodes.map { cache.read(it) }.flowOn(Dispatchers.IO).buffer(Channel.UNLIMITED)
.map { storage.cache.read(it) }
.flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED)
val cacheFlow = val cacheFlow =
cacheResults.divert { cacheResults.divert {
when (it) { when (it) {
@ -82,7 +87,7 @@ private class ExtractStepImpl(
.mapNotNull { file -> .mapNotNull { file ->
val metadata = metadataExtractor.extract(file) ?: return@mapNotNull null val metadata = metadataExtractor.extract(file) ?: return@mapNotNull null
val tags = tagParser.parse(file, metadata) val tags = tagParser.parse(file, metadata)
val cover = metadata.cover?.let { storage.storedCovers.write(it) } val cover = metadata.cover?.let { storedCovers.write(it) }
RawSong(file, metadata.properties, tags, cover) RawSong(file, metadata.properties, tags, cover)
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
@ -91,7 +96,7 @@ private class ExtractStepImpl(
val writtenSongs = val writtenSongs =
merge(*extractedSongs) merge(*extractedSongs)
.map { .map {
storage.cache.write(it) cache.write(it)
ExtractedMusic.Song(it) ExtractedMusic.Song(it)
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)

View file

@ -21,33 +21,27 @@ package org.oxycblt.musikr.playlist.interpret
import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.PlaylistHandle import org.oxycblt.musikr.playlist.PlaylistHandle
import org.oxycblt.musikr.tag.interpret.Naming
internal interface PlaylistInterpreter { internal interface PlaylistInterpreter {
fun interpret(file: PlaylistFile, interpretation: Interpretation): PrePlaylist fun interpret(file: PlaylistFile): PrePlaylist
fun interpret( fun interpret(name: String, handle: PlaylistHandle): PostPlaylist
name: String,
handle: PlaylistHandle,
interpretation: Interpretation
): PostPlaylist
companion object { companion object {
fun new(): PlaylistInterpreter = PlaylistInterpreterImpl fun new(interpretation: Interpretation): PlaylistInterpreter =
PlaylistInterpreterImpl(interpretation.naming)
} }
} }
private data object PlaylistInterpreterImpl : PlaylistInterpreter { private class PlaylistInterpreterImpl(private val naming: Naming) : PlaylistInterpreter {
override fun interpret(file: PlaylistFile, interpretation: Interpretation) = override fun interpret(file: PlaylistFile) =
PrePlaylist( PrePlaylist(
name = interpretation.naming.name(file.name, null), name = naming.name(file.name, null),
rawName = file.name, rawName = file.name,
handle = file.handle, handle = file.handle,
songPointers = file.songPointers) songPointers = file.songPointers)
override fun interpret( override fun interpret(name: String, handle: PlaylistHandle): PostPlaylist =
name: String, PostPlaylist(name = naming.name(name, null), rawName = name, handle = handle)
handle: PlaylistHandle,
interpretation: Interpretation
): PostPlaylist =
PostPlaylist(name = interpretation.naming.name(name, null), rawName = name, handle = handle)
} }

View file

@ -31,15 +31,15 @@ import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.util.toUuidOrNull import org.oxycblt.musikr.util.toUuidOrNull
internal interface TagInterpreter { internal interface TagInterpreter {
fun interpret(song: RawSong, interpretation: Interpretation): PreSong fun interpret(song: RawSong): PreSong
companion object { companion object {
fun new(): TagInterpreter = TagInterpreterImpl fun new(interpretation: Interpretation): TagInterpreter = TagInterpreterImpl(interpretation)
} }
} }
private data object TagInterpreterImpl : TagInterpreter { private class TagInterpreterImpl(private val interpretation: Interpretation) : TagInterpreter {
override fun interpret(song: RawSong, interpretation: Interpretation): PreSong { override fun interpret(song: RawSong): PreSong {
val individualPreArtists = val individualPreArtists =
makePreArtists( makePreArtists(
song.tags.artistMusicBrainzIds, song.tags.artistMusicBrainzIds,