musikr: add full playlist evaluation

This commit is contained in:
Alexander Capehart 2024-12-16 20:45:26 -05:00
parent 3d94ab67cf
commit 7fab7f7eeb
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 148 additions and 30 deletions

View file

@ -18,6 +18,7 @@
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 {
@ -27,6 +28,8 @@ 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

@ -80,6 +80,6 @@ private class MusikrImpl(
.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(interpretation, extracted) evaluateStep.evaluate(storage, interpretation, extracted)
} }
} }

View file

@ -22,6 +22,7 @@ import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.SongPointer import org.oxycblt.musikr.playlist.SongPointer
import org.oxycblt.musikr.playlist.interpret.PrePlaylist
import org.oxycblt.musikr.tag.interpret.PreAlbum import org.oxycblt.musikr.tag.interpret.PreAlbum
import org.oxycblt.musikr.tag.interpret.PreArtist import org.oxycblt.musikr.tag.interpret.PreArtist
import org.oxycblt.musikr.tag.interpret.PreGenre import org.oxycblt.musikr.tag.interpret.PreGenre
@ -32,12 +33,13 @@ internal data class MusicGraph(
val songVertex: List<SongVertex>, val songVertex: List<SongVertex>,
val albumVertex: List<AlbumVertex>, val albumVertex: List<AlbumVertex>,
val artistVertex: List<ArtistVertex>, val artistVertex: List<ArtistVertex>,
val genreVertex: List<GenreVertex> val genreVertex: List<GenreVertex>,
val playlistVertex: Set<PlaylistVertex>
) { ) {
interface Builder { interface Builder {
fun add(preSong: PreSong) fun add(preSong: PreSong)
fun add(file: PlaylistFile) fun add(prePlaylist: PrePlaylist)
fun build(): MusicGraph fun build(): MusicGraph
} }
@ -108,8 +110,8 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
songVertices[uid] = songVertex songVertices[uid] = songVertex
} }
override fun add(file: PlaylistFile) { override fun add(prePlaylist: PrePlaylist) {
playlistVertices.add(PlaylistVertex(file)) playlistVertices.add(PlaylistVertex(prePlaylist))
} }
override fun build(): MusicGraph { override fun build(): MusicGraph {
@ -152,7 +154,8 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
songVertices.values.toList(), songVertices.values.toList(),
albumVertices.values.toList(), albumVertices.values.toList(),
artistVertices.values.toList(), artistVertices.values.toList(),
genreVertices.values.toList()) genreVertices.values.toList(),
playlistVertices)
} }
private fun simplifyGenreCluster(cluster: Collection<GenreVertex>) { private fun simplifyGenreCluster(cluster: Collection<GenreVertex>) {
@ -338,7 +341,7 @@ internal class GenreVertex(val preGenre: PreGenre) {
var tag: Any? = null var tag: Any? = null
} }
internal class PlaylistVertex(val file: PlaylistFile) { internal class PlaylistVertex(val prePlaylist: PrePlaylist) {
val songVertices = mutableListOf<SongVertex?>() val songVertices = mutableListOf<SongVertex?>()
val pointerMap = mutableMapOf<SongPointer, Int>() val pointerMap = mutableMapOf<SongPointer, Int>()
val tag: Any? = null val tag: Any? = null

View file

@ -21,16 +21,19 @@ 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.SongVertex import org.oxycblt.musikr.graph.SongVertex
internal interface LibraryFactory { internal interface LibraryFactory {
fun create(graph: MusicGraph): MutableLibrary fun create(graph: MusicGraph, storage: Storage, interpretation: Interpretation): MutableLibrary
companion object { companion object {
fun new(): LibraryFactory = LibraryFactoryImpl() fun new(): LibraryFactory = LibraryFactoryImpl()
@ -38,7 +41,7 @@ internal interface LibraryFactory {
} }
private class LibraryFactoryImpl() : LibraryFactory { private class LibraryFactoryImpl() : LibraryFactory {
override fun create(graph: MusicGraph): MutableLibrary { override fun create(graph: MusicGraph, storage: Storage, interpretation: Interpretation): MutableLibrary {
val songs = val songs =
graph.songVertex.mapTo(mutableSetOf()) { vertex -> graph.songVertex.mapTo(mutableSetOf()) { vertex ->
SongImpl(SongVertexCore(vertex)).also { vertex.tag = it } SongImpl(SongVertexCore(vertex)).also { vertex.tag = it }
@ -55,7 +58,11 @@ private class LibraryFactoryImpl() : LibraryFactory {
graph.genreVertex.mapTo(mutableSetOf()) { vertex -> graph.genreVertex.mapTo(mutableSetOf()) { vertex ->
GenreImpl(GenreVertexCore(vertex)).also { vertex.tag = it } GenreImpl(GenreVertexCore(vertex)).also { vertex.tag = it }
} }
return LibraryImpl(songs, albums, artists, genres) val playlists =
graph.playlistVertex.mapTo(mutableSetOf()) { vertex ->
PlaylistImpl(PlaylistVertexCore(vertex))
}
return LibraryImpl(songs, albums, artists, genres, playlists, storage, interpretation)
} }
private class SongVertexCore(private val vertex: SongVertex) : SongCore { private class SongVertexCore(private val vertex: SongVertex) : SongCore {
@ -94,4 +101,12 @@ private class LibraryFactoryImpl() : LibraryFactory {
override val artists = vertex.artistVertices.mapTo(mutableSetOf()) { it.tag as Artist } override val artists = vertex.artistVertices.mapTo(mutableSetOf()) { it.tag as Artist }
} }
private class PlaylistVertexCore(vertex: PlaylistVertex) : PlaylistCore {
override val prePlaylist = vertex.prePlaylist
override val songs = vertex.songVertices.mapNotNull { vertex ->
vertex?.let { it.tag as Song }
}
}
} }

View file

@ -18,26 +18,35 @@
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.cover.StoredCovers
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.playlist.PlaylistHandle
import org.oxycblt.musikr.playlist.db.PlaylistInfo
import org.oxycblt.musikr.playlist.db.StoredPlaylists
internal class LibraryImpl( internal data class LibraryImpl(
override val songs: Collection<SongImpl>, override val songs: Collection<SongImpl>,
override val albums: Collection<AlbumImpl>, override val albums: Collection<AlbumImpl>,
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>,
private val storage: Storage,
private val interpretation: Interpretation
) : MutableLibrary { ) : MutableLibrary {
override val playlists = emptySet<Playlist>()
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 }
private val artistUidMap = artists.associateBy { it.uid } private val artistUidMap = artists.associateBy { it.uid }
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 }

View file

@ -22,17 +22,16 @@ import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.playlist.PlaylistHandle import org.oxycblt.musikr.playlist.PlaylistHandle
import org.oxycblt.musikr.playlist.interpret.PrePlaylistInfo
import org.oxycblt.musikr.tag.Name import org.oxycblt.musikr.tag.Name
import org.oxycblt.musikr.tag.interpret.PrePlaylist
internal interface PlaylistCore { internal interface PlaylistCore {
val prePlaylist: PrePlaylist val prePlaylist: PrePlaylistInfo
val handle: PlaylistHandle
val songs: List<Song> val songs: List<Song>
} }
internal class PlaylistImpl(private val core: PlaylistCore) : Playlist { internal class PlaylistImpl(private val core: PlaylistCore) : Playlist {
override val uid = core.handle.uid override val uid = core.prePlaylist.handle.uid
override val name: Name.Known = core.prePlaylist.name override val name: Name.Known = core.prePlaylist.name
override val durationMs = core.songs.sumOf { it.durationMs } override val durationMs = core.songs.sumOf { it.durationMs }
override val cover = Cover.multi(core.songs) override val cover = Cover.multi(core.songs)

View file

@ -30,28 +30,33 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.MutableLibrary import org.oxycblt.musikr.MutableLibrary
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.model.PlaylistImpl import org.oxycblt.musikr.model.PlaylistImpl
import org.oxycblt.musikr.playlist.SongPointer import org.oxycblt.musikr.playlist.SongPointer
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(
storage: Storage,
interpretation: Interpretation, interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic> extractedMusic: Flow<ExtractedMusic>
): MutableLibrary ): MutableLibrary
companion object { companion object {
fun new(): EvaluateStep = EvaluateStepImpl(TagInterpreter.new(), LibraryFactory.new()) fun new(): EvaluateStep = EvaluateStepImpl(TagInterpreter.new(), PlaylistInterpreter.new(), LibraryFactory.new())
} }
} }
private class EvaluateStepImpl( private class EvaluateStepImpl(
private val tagInterpreter: TagInterpreter, private val tagInterpreter: TagInterpreter,
private val playlistInterpreter: PlaylistInterpreter,
private val libraryFactory: LibraryFactory private val libraryFactory: LibraryFactory
) : EvaluateStep { ) : EvaluateStep {
override suspend fun evaluate( override suspend fun evaluate(
storage: Storage,
interpretation: Interpretation, interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic> extractedMusic: Flow<ExtractedMusic>
): MutableLibrary { ): MutableLibrary {
@ -67,14 +72,17 @@ private class EvaluateStepImpl(
.map { tagInterpreter.interpret(it, interpretation) } .map { tagInterpreter.interpret(it, interpretation) }
.flowOn(Dispatchers.Main) .flowOn(Dispatchers.Main)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
val playlistFiles = filterFlow.left val prePlaylists = filterFlow.left
.map { playlistInterpreter.interpret(it, interpretation) }
.flowOn(Dispatchers.Main)
.buffer(Channel.UNLIMITED)
val graphBuilder = MusicGraph.builder() val graphBuilder = MusicGraph.builder()
val graphBuild = merge( val graphBuild = merge(
preSongs.onEach { graphBuilder.add(it) }, preSongs.onEach { graphBuilder.add(it) },
playlistFiles.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) return libraryFactory.create(graph, storage, interpretation)
} }
} }

View file

@ -18,10 +18,14 @@
package org.oxycblt.musikr.playlist.db package org.oxycblt.musikr.playlist.db
import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Song
import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.PlaylistHandle
import org.oxycblt.musikr.playlist.SongPointer import org.oxycblt.musikr.playlist.SongPointer
interface StoredPlaylists { interface StoredPlaylists {
suspend fun new(name: String, songs: List<Song>): PlaylistHandle
suspend fun read(): List<PlaylistFile> suspend fun read(): List<PlaylistFile>
companion object { companion object {
@ -31,11 +35,27 @@ interface StoredPlaylists {
} }
private class StoredPlaylistsImpl(private val playlistDao: PlaylistDao) : StoredPlaylists { private class StoredPlaylistsImpl(private val playlistDao: PlaylistDao) : StoredPlaylists {
override suspend fun new(name: String, songs: List<Song>): PlaylistHandle {
val info =
PlaylistInfo(
Music.UID.auxio(Music.UID.Item.PLAYLIST),
name
)
playlistDao.insertPlaylist(
RawPlaylist(
info,
songs.map { PlaylistSong(it.uid) }
)
)
return StoredPlaylistHandle(info, playlistDao)
}
override suspend fun read() = override suspend fun read() =
playlistDao.readRawPlaylists().map { playlistDao.readRawPlaylists().map {
PlaylistFile( PlaylistFile(
it.playlistInfo.name, it.playlistInfo.name,
it.songs.map { song -> SongPointer.UID(song.songUid) }, it.songs.map { song -> SongPointer.UID(song.songUid) },
StoredPlaylistHandle(it.playlistInfo, playlistDao)) StoredPlaylistHandle(it.playlistInfo, playlistDao)
)
} }
} }

View file

@ -0,0 +1,42 @@
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.playlist.SongPointer
internal interface PlaylistInterpreter {
fun interpret(file: PlaylistFile, interpretation: Interpretation): PrePlaylist
fun interpret(
name: String,
handle: PlaylistHandle,
interpretation: Interpretation
): PostPlaylist
companion object {
fun new(): PlaylistInterpreter = PlaylistInterpreterImpl
}
}
private data object PlaylistInterpreterImpl : PlaylistInterpreter {
override fun interpret(file: PlaylistFile, interpretation: Interpretation) =
PrePlaylist(
name = interpretation.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
)
}

View file

@ -0,0 +1,24 @@
package org.oxycblt.musikr.playlist.interpret
import org.oxycblt.musikr.playlist.PlaylistHandle
import org.oxycblt.musikr.playlist.SongPointer
import org.oxycblt.musikr.tag.Name
internal interface PrePlaylistInfo {
val name: Name.Known
val rawName: String?
val handle: PlaylistHandle
}
internal data class PrePlaylist(
override val name: Name.Known,
override val rawName: String?,
override val handle: PlaylistHandle,
val songPointers: List<SongPointer>
) : PrePlaylistInfo
internal data class PostPlaylist(
override val name: Name.Known,
override val rawName: String?,
override val handle: PlaylistHandle,
) : PrePlaylistInfo

View file

@ -84,9 +84,3 @@ internal data class PreGenre(
val name: Name, val name: Name,
val rawName: String?, val rawName: String?,
) )
internal data class PrePlaylist(
val name: Name.Known,
val rawName: String?,
val handle: PlaylistHandle
)

View file

@ -21,6 +21,7 @@ package org.oxycblt.musikr.tag.interpret
import org.oxycblt.musikr.Interpretation import org.oxycblt.musikr.Interpretation
import org.oxycblt.musikr.fs.Format import org.oxycblt.musikr.fs.Format
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.tag.Disc import org.oxycblt.musikr.tag.Disc
import org.oxycblt.musikr.tag.Name import org.oxycblt.musikr.tag.Name
import org.oxycblt.musikr.tag.Placeholder import org.oxycblt.musikr.tag.Placeholder