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.components.SingletonComponent
import javax.inject.Singleton
import org.oxycblt.musikr.Musikr
import org.oxycblt.musikr.cache.CacheDatabase
import org.oxycblt.musikr.playlist.db.PlaylistDatabase
@ -48,6 +47,4 @@ class MusikrShimModule {
@Singleton
@Provides
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 {
val library: Library?
/** The current state of music loading. Null if no load has occurred yet. */
val indexingState: IndexingState?
@ -212,7 +213,6 @@ interface MusicRepository {
class MusicRepositoryImpl
@Inject
constructor(
private val musikr: Musikr,
@ApplicationContext private val context: Context,
private val cacheDatabase: CacheDatabase,
private val playlistDatabase: PlaylistDatabase,
@ -375,9 +375,11 @@ constructor(
StoredCovers.from(context, "covers"),
StoredPlaylists.from(playlistDatabase))
}
val interpretation = Interpretation(nameFactory, separators)
val newLibrary =
musikr.run(
locations, storage, Interpretation(nameFactory, separators), ::emitIndexingProgress)
Musikr.new(context, storage, interpretation).run(locations, ::emitIndexingProgress)
emitIndexingCompletion(null)

View file

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

View file

@ -33,14 +33,15 @@ import org.oxycblt.musikr.pipeline.ExtractStep
interface Musikr {
suspend fun run(
locations: List<MusicLocation>,
storage: Storage,
interpretation: Interpretation,
onProgress: suspend (IndexingProgress) -> Unit = {}
): MutableLibrary
companion object {
fun new(context: Context): Musikr =
MusikrImpl(ExploreStep.from(context), ExtractStep.from(context), EvaluateStep.new())
fun new(context: Context, storage: Storage, interpretation: Interpretation): Musikr =
MusikrImpl(
ExploreStep.from(context, storage),
ExtractStep.from(context, storage),
EvaluateStep.new(storage, interpretation))
}
}
@ -62,24 +63,22 @@ private class MusikrImpl(
) : Musikr {
override suspend fun run(
locations: List<MusicLocation>,
storage: Storage,
interpretation: Interpretation,
onProgress: suspend (IndexingProgress) -> Unit
) = coroutineScope {
var exploredCount = 0
var extractedCount = 0
val explored =
exploreStep
.explore(locations, storage)
.explore(locations)
.buffer(Channel.UNLIMITED)
.onStart { onProgress(IndexingProgress.Songs(0, 0)) }
.onEach { onProgress(IndexingProgress.Songs(extractedCount, ++exploredCount)) }
val extracted =
extractStep
.extract(storage, explored)
.extract(explored)
.buffer(Channel.UNLIMITED)
.onEach { onProgress(IndexingProgress.Songs(++extractedCount, exploredCount)) }
.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.Artist
import org.oxycblt.musikr.Genre
import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.MutableLibrary
import org.oxycblt.musikr.Song
import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.graph.AlbumVertex
import org.oxycblt.musikr.graph.ArtistVertex
import org.oxycblt.musikr.graph.GenreVertex
import org.oxycblt.musikr.graph.MusicGraph
import org.oxycblt.musikr.graph.PlaylistVertex
import org.oxycblt.musikr.graph.SongVertex
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter
internal interface LibraryFactory {
fun create(graph: MusicGraph, storage: Storage, interpretation: Interpretation): MutableLibrary
fun create(
graph: MusicGraph,
storedPlaylists: StoredPlaylists,
playlistInterpreter: PlaylistInterpreter
): MutableLibrary
companion object {
fun new(): LibraryFactory = LibraryFactoryImpl()
@ -43,8 +47,8 @@ internal interface LibraryFactory {
private class LibraryFactoryImpl() : LibraryFactory {
override fun create(
graph: MusicGraph,
storage: Storage,
interpretation: Interpretation
storedPlaylists: StoredPlaylists,
playlistInterpreter: PlaylistInterpreter
): MutableLibrary {
val songs =
graph.songVertex.mapTo(mutableSetOf()) { vertex ->
@ -66,7 +70,8 @@ private class LibraryFactoryImpl() : LibraryFactory {
graph.playlistVertex.mapTo(mutableSetOf()) { 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 {

View file

@ -18,13 +18,14 @@
package org.oxycblt.musikr.model
import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.Music
import org.oxycblt.musikr.MutableLibrary
import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song
import org.oxycblt.musikr.Storage
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(
override val songs: Collection<SongImpl>,
@ -32,8 +33,8 @@ internal data class LibraryImpl(
override val artists: Collection<ArtistImpl>,
override val genres: Collection<GenreImpl>,
override val playlists: Collection<Playlist>,
private val storage: Storage,
private val interpretation: Interpretation
private val storedPlaylists: StoredPlaylists,
private val playlistInterpreter: PlaylistInterpreter
) : MutableLibrary {
private val songUidMap = songs.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 playlistUidMap = playlists.associateBy { it.uid }
override val storedCovers = storage.storedCovers
override fun findSong(uid: Music.UID) = songUidMap[uid]
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 {
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.graph.MusicGraph
import org.oxycblt.musikr.model.LibraryFactory
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.interpret.PlaylistInterpreter
import org.oxycblt.musikr.tag.interpret.TagInterpreter
internal interface EvaluateStep {
suspend fun evaluate(
storage: Storage,
interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic>
): MutableLibrary
suspend fun evaluate(extractedMusic: Flow<ExtractedMusic>): MutableLibrary
companion object {
fun new(): EvaluateStep =
EvaluateStepImpl(TagInterpreter.new(), PlaylistInterpreter.new(), LibraryFactory.new())
fun new(storage: Storage, interpretation: Interpretation): EvaluateStep =
EvaluateStepImpl(
TagInterpreter.new(interpretation),
PlaylistInterpreter.new(interpretation),
storage.storedPlaylists,
LibraryFactory.new())
}
}
private class EvaluateStepImpl(
private val tagInterpreter: TagInterpreter,
private val playlistInterpreter: PlaylistInterpreter,
private val storedPlaylists: StoredPlaylists,
private val libraryFactory: LibraryFactory
) : EvaluateStep {
override suspend fun evaluate(
storage: Storage,
interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic>
): MutableLibrary {
override suspend fun evaluate(extractedMusic: Flow<ExtractedMusic>): MutableLibrary {
val filterFlow =
extractedMusic.divert {
when (it) {
@ -68,12 +66,12 @@ private class EvaluateStepImpl(
val rawSongs = filterFlow.right
val preSongs =
rawSongs
.map { tagInterpreter.interpret(it, interpretation) }
.map { tagInterpreter.interpret(it) }
.flowOn(Dispatchers.Default)
.buffer(Channel.UNLIMITED)
val prePlaylists =
filterFlow.left
.map { playlistInterpreter.interpret(it, interpretation) }
.map { playlistInterpreter.interpret(it) }
.flowOn(Dispatchers.Default)
.buffer(Channel.UNLIMITED)
val graphBuilder = MusicGraph.builder()
@ -84,6 +82,6 @@ private class EvaluateStepImpl(
prePlaylists.onEach { graphBuilder.add(it) })
graphBuild.collect()
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.query.DeviceFiles
import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.db.StoredPlaylists
import org.oxycblt.musikr.playlist.m3u.M3U
internal interface ExploreStep {
fun explore(locations: List<MusicLocation>, storage: Storage): Flow<ExploreNode>
fun explore(locations: List<MusicLocation>): Flow<ExploreNode>
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 {
override fun explore(locations: List<MusicLocation>, storage: Storage): Flow<ExploreNode> {
private class ExploreStepImpl(
private val deviceFiles: DeviceFiles,
private val storedPlaylists: StoredPlaylists
) : ExploreStep {
override fun explore(locations: List<MusicLocation>): Flow<ExploreNode> {
val audios =
deviceFiles
.explore(locations.asFlow())
@ -59,7 +64,7 @@ private class ExploreStepImpl(private val deviceFiles: DeviceFiles) : ExploreSte
.flowOn(Dispatchers.IO)
.buffer()
val playlists =
flow { emitAll(storage.storedPlaylists.read().asFlow()) }
flow { emitAll(storedPlaylists.read().asFlow()) }
.map { ExploreNode.Playlist(it) }
.flowOn(Dispatchers.IO)
.buffer()

View file

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

View file

@ -21,33 +21,27 @@ package org.oxycblt.musikr.playlist.interpret
import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.PlaylistHandle
import org.oxycblt.musikr.tag.interpret.Naming
internal interface PlaylistInterpreter {
fun interpret(file: PlaylistFile, interpretation: Interpretation): PrePlaylist
fun interpret(file: PlaylistFile): PrePlaylist
fun interpret(
name: String,
handle: PlaylistHandle,
interpretation: Interpretation
): PostPlaylist
fun interpret(name: String, handle: PlaylistHandle): PostPlaylist
companion object {
fun new(): PlaylistInterpreter = PlaylistInterpreterImpl
fun new(interpretation: Interpretation): PlaylistInterpreter =
PlaylistInterpreterImpl(interpretation.naming)
}
}
private data object PlaylistInterpreterImpl : PlaylistInterpreter {
override fun interpret(file: PlaylistFile, interpretation: Interpretation) =
private class PlaylistInterpreterImpl(private val naming: Naming) : PlaylistInterpreter {
override fun interpret(file: PlaylistFile) =
PrePlaylist(
name = interpretation.naming.name(file.name, null),
name = naming.name(file.name, null),
rawName = file.name,
handle = file.handle,
songPointers = file.songPointers)
override fun interpret(
name: String,
handle: PlaylistHandle,
interpretation: Interpretation
): PostPlaylist =
PostPlaylist(name = interpretation.naming.name(name, null), rawName = name, handle = handle)
override fun interpret(name: String, handle: PlaylistHandle): PostPlaylist =
PostPlaylist(name = 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
internal interface TagInterpreter {
fun interpret(song: RawSong, interpretation: Interpretation): PreSong
fun interpret(song: RawSong): PreSong
companion object {
fun new(): TagInterpreter = TagInterpreterImpl
fun new(interpretation: Interpretation): TagInterpreter = TagInterpreterImpl(interpretation)
}
}
private data object TagInterpreterImpl : TagInterpreter {
override fun interpret(song: RawSong, interpretation: Interpretation): PreSong {
private class TagInterpreterImpl(private val interpretation: Interpretation) : TagInterpreter {
override fun interpret(song: RawSong): PreSong {
val individualPreArtists =
makePreArtists(
song.tags.artistMusicBrainzIds,