musikr: re-implement playlist graphing

This commit is contained in:
Alexander Capehart 2024-12-16 20:13:08 -05:00
parent a50b55cf70
commit 3d94ab67cf
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 73 additions and 16 deletions

View file

@ -41,7 +41,7 @@ spotless {
cpp { cpp {
target "*/src/**/cpp/*.cpp" target "*/src/**/cpp/*.cpp"
clangFormat("18.1.8") clangFormat("18.1.8").
licenseHeaderFile("NOTICE") licenseHeaderFile("NOTICE")
} }
} }

View file

@ -19,6 +19,9 @@
package org.oxycblt.musikr.graph package org.oxycblt.musikr.graph
import org.oxycblt.musikr.Music import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.playlist.SongPointer
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
@ -34,6 +37,8 @@ internal data class MusicGraph(
interface Builder { interface Builder {
fun add(preSong: PreSong) fun add(preSong: PreSong)
fun add(file: PlaylistFile)
fun build(): MusicGraph fun build(): MusicGraph
} }
@ -47,6 +52,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
private val albumVertices = mutableMapOf<PreAlbum, AlbumVertex>() private val albumVertices = mutableMapOf<PreAlbum, AlbumVertex>()
private val artistVertices = mutableMapOf<PreArtist, ArtistVertex>() private val artistVertices = mutableMapOf<PreArtist, ArtistVertex>()
private val genreVertices = mutableMapOf<PreGenre, GenreVertex>() private val genreVertices = mutableMapOf<PreGenre, GenreVertex>()
private val playlistVertices = mutableSetOf<PlaylistVertex>()
override fun add(preSong: PreSong) { override fun add(preSong: PreSong) {
val uid = preSong.computeUid() val uid = preSong.computeUid()
@ -102,6 +108,10 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
songVertices[uid] = songVertex songVertices[uid] = songVertex
} }
override fun add(file: PlaylistFile) {
playlistVertices.add(PlaylistVertex(file))
}
override fun build(): MusicGraph { override fun build(): MusicGraph {
val genreClusters = genreVertices.values.groupBy { it.preGenre.rawName?.lowercase() } val genreClusters = genreVertices.values.groupBy { it.preGenre.rawName?.lowercase() }
for (cluster in genreClusters.values) { for (cluster in genreClusters.values) {
@ -124,9 +134,18 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder {
it.artistVertices = it.artistVertices.distinct().toMutableList() it.artistVertices = it.artistVertices.distinct().toMutableList()
} }
songVertices.values.forEach { songVertices.entries.forEach { entry ->
it.artistVertices = it.artistVertices.distinct().toMutableList() val vertex = entry.value
it.genreVertices = it.genreVertices.distinct().toMutableList() vertex.artistVertices = vertex.artistVertices.distinct().toMutableList()
vertex.genreVertices = vertex.genreVertices.distinct().toMutableList()
playlistVertices.forEach {
val pointer = SongPointer.UID(entry.key)
val index = it.pointerMap[pointer]
if (index != null) {
it.songVertices[index] = vertex
}
}
} }
return MusicGraph( return MusicGraph(
@ -318,3 +337,9 @@ internal class GenreVertex(val preGenre: PreGenre) {
val artistVertices = mutableSetOf<ArtistVertex>() val artistVertices = mutableSetOf<ArtistVertex>()
var tag: Any? = null var tag: Any? = null
} }
internal class PlaylistVertex(val file: PlaylistFile) {
val songVertices = mutableListOf<SongVertex?>()
val pointerMap = mutableMapOf<SongPointer, Int>()
val tag: Any? = null
}

View file

@ -22,13 +22,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
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.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.playlist.SongPointer
import org.oxycblt.musikr.tag.interpret.TagInterpreter import org.oxycblt.musikr.tag.interpret.TagInterpreter
internal interface EvaluateStep { internal interface EvaluateStep {
@ -50,14 +55,25 @@ private class EvaluateStepImpl(
interpretation: Interpretation, interpretation: Interpretation,
extractedMusic: Flow<ExtractedMusic> extractedMusic: Flow<ExtractedMusic>
): MutableLibrary { ): MutableLibrary {
val filterFlow = extractedMusic.divert {
when (it) {
is ExtractedMusic.Song -> Divert.Right(it.song)
is ExtractedMusic.Playlist -> Divert.Left(it.file)
}
}
val rawSongs = filterFlow.right
val preSongs = val preSongs =
extractedMusic rawSongs
.filterIsInstance<ExtractedMusic.Song>() .map { tagInterpreter.interpret(it, interpretation) }
.map { tagInterpreter.interpret(it.song, interpretation) }
.flowOn(Dispatchers.Main) .flowOn(Dispatchers.Main)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
val playlistFiles = filterFlow.left
val graphBuilder = MusicGraph.builder() val graphBuilder = MusicGraph.builder()
preSongs.collect { graphBuilder.add(it) } val graphBuild = merge(
preSongs.onEach { graphBuilder.add(it) },
playlistFiles.onEach { graphBuilder.add(it) }
)
graphBuild.collect()
val graph = graphBuilder.build() val graph = graphBuilder.build()
return libraryFactory.create(graph) return libraryFactory.create(graph)
} }

View file

@ -34,6 +34,7 @@ import org.oxycblt.musikr.cover.Cover
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
import org.oxycblt.musikr.playlist.PlaylistFile
import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.parse.TagParser import org.oxycblt.musikr.tag.parse.TagParser
@ -51,21 +52,29 @@ private class ExtractStepImpl(
private val tagParser: TagParser private val tagParser: TagParser
) : ExtractStep { ) : ExtractStep {
override fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic> { override fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
val filterFlow = nodes.divert {
when (it) {
is ExploreNode.Audio -> Divert.Right(it.file)
is ExploreNode.Playlist -> Divert.Left(it.file)
}
}
val audioNodes = filterFlow.right
val playlistNodes = filterFlow.left.map { ExtractedMusic.Playlist(it) }
val cacheResults = val cacheResults =
nodes audioNodes
.filterIsInstance<ExploreNode.Audio>() .map { storage.cache.read(it) }
.map { storage.cache.read(it.file) }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
val divertedFlow = val cacheFlow =
cacheResults.divert { cacheResults.divert {
when (it) { when (it) {
is CacheResult.Hit -> Divert.Left(it.song) is CacheResult.Hit -> Divert.Left(it.song)
is CacheResult.Miss -> Divert.Right(it.file) is CacheResult.Miss -> Divert.Right(it.file)
} }
} }
val cachedSongs = divertedFlow.left.map { ExtractedMusic.Song(it) } val cachedSongs = cacheFlow.left.map { ExtractedMusic.Song(it) }
val uncachedSongs = divertedFlow.right val uncachedSongs = cacheFlow.right
val distributedFlow = uncachedSongs.distribute(16) val distributedFlow = uncachedSongs.distribute(16)
val extractedSongs = val extractedSongs =
Array(distributedFlow.flows.size) { i -> Array(distributedFlow.flows.size) { i ->
@ -87,8 +96,13 @@ private class ExtractStepImpl(
} }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
return merge<ExtractedMusic>( return merge(
divertedFlow.manager, cachedSongs, distributedFlow.manager, writtenSongs) filterFlow.manager,
cacheFlow.manager,
cachedSongs,
distributedFlow.manager,
writtenSongs,
playlistNodes)
} }
} }
@ -101,4 +115,6 @@ data class RawSong(
internal sealed interface ExtractedMusic { internal sealed interface ExtractedMusic {
data class Song(val song: RawSong) : ExtractedMusic data class Song(val song: RawSong) : ExtractedMusic
data class Playlist(val file: PlaylistFile) : ExtractedMusic
} }