musikr: refactor graphing

The sub-elements are still very deeply coupled, but that's to be
expected.

What this enables is reasonable testing of the graphing system, given
that it's easily the most brittle part of musikr.
This commit is contained in:
Alexander Capehart 2025-01-22 12:19:49 -07:00
parent 3ff662ac27
commit 55d3bd79ba
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 154 additions and 134 deletions

View file

@ -22,27 +22,27 @@ import org.oxycblt.musikr.tag.interpret.PreAlbum
import org.oxycblt.musikr.util.unlikelyToBeNull import org.oxycblt.musikr.util.unlikelyToBeNull
internal class AlbumGraph(private val artistGraph: ArtistGraph) { internal class AlbumGraph(private val artistGraph: ArtistGraph) {
val vertices = mutableMapOf<PreAlbum, AlbumVertex>() private val vertices = mutableMapOf<PreAlbum, AlbumVertex>()
fun add(preAlbum: PreAlbum): AlbumVertex { fun link(vertex: SongVertex) {
// Albums themselves have their own parent artists that also need to be // Albums themselves have their own parent artists that also need to be
// linked up. // linked up.
val albumartistGraph = preAlbum.preArtists.map { preArtist -> artistGraph.add(preArtist) } val preAlbum = vertex.preSong.preAlbum
val albumVertex = AlbumVertex(preAlbum, albumartistGraph.toMutableList()) val albumVertex =
// Album vertex is linked, now link artists back to album. vertices.getOrPut(preAlbum) { AlbumVertex(preAlbum).also { artistGraph.link(it) } }
albumartistGraph.forEach { artistVertex -> artistVertex.albumGraph.add(albumVertex) } vertex.albumVertex = albumVertex
return albumVertex albumVertex.songVertices.add(vertex)
} }
fun simplify() { fun solve(): Collection<AlbumVertex> {
val albumClusters = vertices.values.groupBy { it.preAlbum.rawName?.lowercase() } val albumClusters = vertices.values.groupBy { it.preAlbum.rawName?.lowercase() }
for (cluster in albumClusters.values) { for (cluster in albumClusters.values) {
simplifyAlbumCluster(cluster) simplifyAlbumCluster(cluster)
} }
// Remove any edges that wound up connecting to the same artist or genre // Remove any edges that wound up connecting to the same artist or genre
// in the end after simplification. // in the end after simplification.
vertices.values.forEach { it.artistGraph = it.artistGraph.distinct().toMutableList() } vertices.values.forEach { it.artistVertices = it.artistVertices.distinct().toMutableList() }
return vertices.values
} }
private fun simplifyAlbumCluster(cluster: Collection<AlbumVertex>) { private fun simplifyAlbumCluster(cluster: Collection<AlbumVertex>) {
@ -65,9 +65,9 @@ internal class AlbumGraph(private val artistGraph: ArtistGraph) {
val noMbidPreAlbum = it.preAlbum.copy(musicBrainzId = null) val noMbidPreAlbum = it.preAlbum.copy(musicBrainzId = null)
val simpleMbidVertex = val simpleMbidVertex =
vertices.getOrPut(noMbidPreAlbum) { vertices.getOrPut(noMbidPreAlbum) {
AlbumVertex(noMbidPreAlbum, it.artistGraph.toMutableList()) AlbumVertex(noMbidPreAlbum).apply { artistVertices = it.artistVertices }
} }
meldAlbumGraph(it, simpleMbidVertex) meldAlbumVertices(it, simpleMbidVertex)
simpleMbidVertex simpleMbidVertex
} }
simplifyAlbumClusterImpl(strippedCluster) simplifyAlbumClusterImpl(strippedCluster)
@ -84,31 +84,33 @@ internal class AlbumGraph(private val artistGraph: ArtistGraph) {
val dst = clusterSet.maxBy { it.songVertices.size } val dst = clusterSet.maxBy { it.songVertices.size }
clusterSet.remove(dst) clusterSet.remove(dst)
for (src in clusterSet) { for (src in clusterSet) {
meldAlbumGraph(src, dst) meldAlbumVertices(src, dst)
} }
} }
private fun meldAlbumGraph(src: AlbumVertex, dst: AlbumVertex) { private fun meldAlbumVertices(src: AlbumVertex, dst: AlbumVertex) {
if (src == dst) { if (src == dst) {
// Same vertex, do nothing // Same vertex, do nothing
return return
} }
// Link all songs and artists from the irrelevant album to the relevant album. // Link all songs and artists from the irrelevant album to the relevant album.
dst.songVertices.addAll(src.songVertices) dst.songVertices.addAll(src.songVertices)
dst.artistGraph.addAll(src.artistGraph) dst.artistVertices.addAll(src.artistVertices)
// Update all songs and artists to point to the relevant album. // Update all songs and artists to point to the relevant album.
src.songVertices.forEach { it.albumVertex = dst } src.songVertices.forEach { it.albumVertex = dst }
src.artistGraph.forEach { src.artistVertices.forEach {
it.albumGraph.remove(src) it.albumVertices.remove(src)
it.albumGraph.add(dst) it.albumVertices.add(dst)
} }
// Remove the irrelevant album from the graph. // Remove the irrelevant album from the graph.
vertices.remove(src.preAlbum) vertices.remove(src.preAlbum)
} }
} }
internal class AlbumVertex(val preAlbum: PreAlbum, var artistGraph: MutableList<ArtistVertex>) : internal class AlbumVertex(
Vertex { val preAlbum: PreAlbum,
) : Vertex {
var artistVertices = mutableListOf<ArtistVertex>()
val songVertices = mutableSetOf<SongVertex>() val songVertices = mutableSetOf<SongVertex>()
override var tag: Any? = null override var tag: Any? = null

View file

@ -21,17 +21,29 @@ package org.oxycblt.musikr.graph
import org.oxycblt.musikr.tag.interpret.PreArtist import org.oxycblt.musikr.tag.interpret.PreArtist
import org.oxycblt.musikr.util.unlikelyToBeNull import org.oxycblt.musikr.util.unlikelyToBeNull
internal class ArtistGraph() { internal class ArtistGraph {
val vertices = mutableMapOf<PreArtist, ArtistVertex>() private val vertices = mutableMapOf<PreArtist, ArtistVertex>()
fun add(preArtist: PreArtist): ArtistVertex = fun link(songVertex: SongVertex) {
vertices.getOrPut(preArtist) { ArtistVertex(preArtist) } val preArtists = songVertex.preSong.preArtists
val artistVertices = preArtists.map { vertices.getOrPut(it) { ArtistVertex(it) } }
songVertex.artistVertices.addAll(artistVertices)
artistVertices.forEach { it.songVertices.add(songVertex) }
}
fun simplify() { fun link(albumVertex: AlbumVertex) {
val preArtists = albumVertex.preAlbum.preArtists
val artistVertices = preArtists.map { vertices.getOrPut(it) { ArtistVertex(it) } }
albumVertex.artistVertices = artistVertices.toMutableList()
artistVertices.forEach { it.albumVertices.add(albumVertex) }
}
fun solve(): Collection<ArtistVertex> {
val artistClusters = vertices.values.groupBy { it.preArtist.rawName?.lowercase() } val artistClusters = vertices.values.groupBy { it.preArtist.rawName?.lowercase() }
for (cluster in artistClusters.values) { for (cluster in artistClusters.values) {
simplifyArtistCluster(cluster) simplifyArtistCluster(cluster)
} }
return vertices.values
} }
private fun simplifyArtistCluster(cluster: Collection<ArtistVertex>) { private fun simplifyArtistCluster(cluster: Collection<ArtistVertex>) {
@ -54,7 +66,7 @@ internal class ArtistGraph() {
val noMbidPreArtist = it.preArtist.copy(musicBrainzId = null) val noMbidPreArtist = it.preArtist.copy(musicBrainzId = null)
val simpleMbidVertex = val simpleMbidVertex =
vertices.getOrPut(noMbidPreArtist) { ArtistVertex(noMbidPreArtist) } vertices.getOrPut(noMbidPreArtist) { ArtistVertex(noMbidPreArtist) }
meldArtistGraph(it, simpleMbidVertex) meldArtistVertices(it, simpleMbidVertex)
simpleMbidVertex simpleMbidVertex
} }
simplifyArtistClusterImpl(strippedCluster) simplifyArtistClusterImpl(strippedCluster)
@ -69,33 +81,33 @@ internal class ArtistGraph() {
val relevantArtistVertex = clusterSet.maxBy { it.songVertices.size } val relevantArtistVertex = clusterSet.maxBy { it.songVertices.size }
clusterSet.remove(relevantArtistVertex) clusterSet.remove(relevantArtistVertex)
for (irrelevantArtistVertex in clusterSet) { for (irrelevantArtistVertex in clusterSet) {
meldArtistGraph(irrelevantArtistVertex, relevantArtistVertex) meldArtistVertices(irrelevantArtistVertex, relevantArtistVertex)
} }
} }
private fun meldArtistGraph(src: ArtistVertex, dst: ArtistVertex) { private fun meldArtistVertices(src: ArtistVertex, dst: ArtistVertex) {
if (src == dst) { if (src == dst) {
// Same vertex, do nothing // Same vertex, do nothing
return return
} }
// Link all songs and albums from the irrelevant artist to the relevant artist. // Link all songs and albums from the irrelevant artist to the relevant artist.
dst.songVertices.addAll(src.songVertices) dst.songVertices.addAll(src.songVertices)
dst.albumGraph.addAll(src.albumGraph) dst.albumVertices.addAll(src.albumVertices)
dst.genreGraph.addAll(src.genreGraph) dst.genreVertices.addAll(src.genreVertices)
// Update all songs, albums, and genres to point to the relevant artist. // Update all songs, albums, and genres to point to the relevant artist.
src.songVertices.forEach { src.songVertices.forEach {
val index = it.artistGraph.indexOf(src) val index = it.artistVertices.indexOf(src)
check(index >= 0) { "Illegal state: directed edge between artist and song" } check(index >= 0) { "Illegal state: directed edge between artist and song" }
it.artistGraph[index] = dst it.artistVertices[index] = dst
} }
src.albumGraph.forEach { src.albumVertices.forEach {
val index = it.artistGraph.indexOf(src) val index = it.artistVertices.indexOf(src)
check(index >= 0) { "Illegal state: directed edge between artist and album" } check(index >= 0) { "Illegal state: directed edge between artist and album" }
it.artistGraph[index] = dst it.artistVertices[index] = dst
} }
src.genreGraph.forEach { src.genreVertices.forEach {
it.artistGraph.remove(src) it.artistVertices.remove(src)
it.artistGraph.add(dst) it.artistVertices.add(dst)
} }
// Remove the irrelevant artist from the graph. // Remove the irrelevant artist from the graph.
@ -107,8 +119,8 @@ internal class ArtistVertex(
val preArtist: PreArtist, val preArtist: PreArtist,
) : Vertex { ) : Vertex {
val songVertices = mutableSetOf<SongVertex>() val songVertices = mutableSetOf<SongVertex>()
val albumGraph = mutableSetOf<AlbumVertex>() val albumVertices = mutableSetOf<AlbumVertex>()
val genreGraph = mutableSetOf<GenreVertex>() val genreVertices = mutableSetOf<GenreVertex>()
override var tag: Any? = null override var tag: Any? = null
override fun toString() = "ArtistVertex(preArtist=$preArtist)" override fun toString() = "ArtistVertex(preArtist=$preArtist)"

View file

@ -21,15 +21,30 @@ package org.oxycblt.musikr.graph
import org.oxycblt.musikr.tag.interpret.PreGenre import org.oxycblt.musikr.tag.interpret.PreGenre
internal class GenreGraph { internal class GenreGraph {
val vertices = mutableMapOf<PreGenre, GenreVertex>() private val vertices = mutableMapOf<PreGenre, GenreVertex>()
fun add(preGenre: PreGenre): GenreVertex = vertices.getOrPut(preGenre) { GenreVertex(preGenre) } fun link(vertex: SongVertex) {
val preGenres = vertex.preSong.preGenres
val artistVertices = vertex.artistVertices
fun simplify() { for (preGenre in preGenres) {
val genreVertex = vertices.getOrPut(preGenre) { GenreVertex(preGenre) }
vertex.genreVertices.add(genreVertex)
genreVertex.songVertices.add(vertex)
for (artistVertex in artistVertices) {
genreVertex.artistVertices.add(artistVertex)
artistVertex.genreVertices.add(genreVertex)
}
}
}
fun solve(): Collection<GenreVertex> {
val genreClusters = vertices.values.groupBy { it.preGenre.rawName?.lowercase() } val genreClusters = vertices.values.groupBy { it.preGenre.rawName?.lowercase() }
for (cluster in genreClusters.values) { for (cluster in genreClusters.values) {
simplifyGenreCluster(cluster) simplifyGenreCluster(cluster)
} }
return vertices.values
} }
private fun simplifyGenreCluster(cluster: Collection<GenreVertex>) { private fun simplifyGenreCluster(cluster: Collection<GenreVertex>) {
@ -43,27 +58,27 @@ internal class GenreGraph {
val dst = clusterSet.maxBy { it.songVertices.size } val dst = clusterSet.maxBy { it.songVertices.size }
clusterSet.remove(dst) clusterSet.remove(dst)
for (src in clusterSet) { for (src in clusterSet) {
meldGenreGraph(src, dst) meldGenreVertices(src, dst)
} }
} }
private fun meldGenreGraph(src: GenreVertex, dst: GenreVertex) { private fun meldGenreVertices(src: GenreVertex, dst: GenreVertex) {
if (src == dst) { if (src == dst) {
// Same vertex, do nothing // Same vertex, do nothing
return return
} }
// Link all songs and artists from the irrelevant genre to the relevant genre. // Link all songs and artists from the irrelevant genre to the relevant genre.
dst.songVertices.addAll(src.songVertices) dst.songVertices.addAll(src.songVertices)
dst.artistGraph.addAll(src.artistGraph) dst.artistVertices.addAll(src.artistVertices)
// Update all songs and artists to point to the relevant genre. // Update all songs and artists to point to the relevant genre.
src.songVertices.forEach { src.songVertices.forEach {
val index = it.genreGraph.indexOf(src) val index = it.genreVertices.indexOf(src)
check(index >= 0) { "Illegal state: directed edge between genre and song" } check(index >= 0) { "Illegal state: directed edge between genre and song" }
it.genreGraph[index] = dst it.genreVertices[index] = dst
} }
src.artistGraph.forEach { src.artistVertices.forEach {
it.genreGraph.remove(src) it.genreVertices.remove(src)
it.genreGraph.add(dst) it.genreVertices.add(dst)
} }
// Remove the irrelevant genre from the graph. // Remove the irrelevant genre from the graph.
vertices.remove(src.preGenre) vertices.remove(src.preGenre)
@ -72,7 +87,7 @@ internal class GenreGraph {
internal class GenreVertex(val preGenre: PreGenre) : Vertex { internal class GenreVertex(val preGenre: PreGenre) : Vertex {
val songVertices = mutableSetOf<SongVertex>() val songVertices = mutableSetOf<SongVertex>()
val artistGraph = mutableSetOf<ArtistVertex>() val artistVertices = mutableSetOf<ArtistVertex>()
override var tag: Any? = null override var tag: Any? = null
override fun toString() = "GenreVertex(preGenre=$preGenre)" override fun toString() = "GenreVertex(preGenre=$preGenre)"

View file

@ -22,11 +22,11 @@ import org.oxycblt.musikr.playlist.interpret.PrePlaylist
import org.oxycblt.musikr.tag.interpret.PreSong import org.oxycblt.musikr.tag.interpret.PreSong
internal data class MusicGraph( internal data class MusicGraph(
val songVertex: List<SongVertex>, val songVertices: Collection<SongVertex>,
val albumVertex: List<AlbumVertex>, val albumVertices: Collection<AlbumVertex>,
val artistVertex: List<ArtistVertex>, val artistVertices: Collection<ArtistVertex>,
val genreVertex: List<GenreVertex>, val genreVertices: Collection<GenreVertex>,
val playlistVertex: Set<PlaylistVertex> val playlistVertices: Collection<PlaylistVertex>
) { ) {
interface Builder { interface Builder {
fun add(preSong: PreSong) fun add(preSong: PreSong)
@ -41,35 +41,32 @@ internal data class MusicGraph(
} }
} }
internal interface Vertex {
val tag: Any?
}
private class MusicGraphBuilderImpl : MusicGraph.Builder { private class MusicGraphBuilderImpl : MusicGraph.Builder {
private val genreGraph = GenreGraph() private val genreGraph = GenreGraph()
private val artistGraph = ArtistGraph() private val artistGraph = ArtistGraph()
private val albumGraph = AlbumGraph(artistGraph) private val albumGraph = AlbumGraph(artistGraph)
private val playlistGraph = PlaylistGraph() private val playlistGraph = PlaylistGraph()
private val songVertices = SongGraph(albumGraph, artistGraph, genreGraph, playlistGraph) private val songGraph = SongGraph(albumGraph, artistGraph, genreGraph, playlistGraph)
override fun add(preSong: PreSong) { override fun add(preSong: PreSong) {
songVertices.add(preSong) songGraph.link(SongVertex(preSong))
} }
override fun add(prePlaylist: PrePlaylist) { override fun add(prePlaylist: PrePlaylist) {
playlistGraph.add(prePlaylist) playlistGraph.link(PlaylistVertex(prePlaylist))
} }
override fun build(): MusicGraph { override fun build(): MusicGraph {
genreGraph.simplify() val genreVertices = genreGraph.solve()
artistGraph.simplify() val artistVertices = artistGraph.solve()
albumGraph.simplify() val albumVertices = albumGraph.solve()
songVertices.simplify() val songVertices = songGraph.solve()
val playlistVertices = playlistGraph.solve()
val graph = return MusicGraph(
MusicGraph( songVertices, albumVertices, artistVertices, genreVertices, playlistVertices)
songVertices.vertices.values.toList(),
albumGraph.vertices.values.toList(),
artistGraph.vertices.values.toList(),
genreGraph.vertices.values.toList(),
playlistGraph.vertices)
return graph
} }
} }

View file

@ -18,14 +18,30 @@
package org.oxycblt.musikr.graph package org.oxycblt.musikr.graph
import org.oxycblt.musikr.playlist.SongPointer
import org.oxycblt.musikr.playlist.interpret.PrePlaylist import org.oxycblt.musikr.playlist.interpret.PrePlaylist
internal class PlaylistGraph { internal class PlaylistGraph {
val vertices = mutableSetOf<PlaylistVertex>() private val pointerMap = mutableMapOf<SongPointer, SongVertex>()
private val vertices = mutableSetOf<PlaylistVertex>()
fun add(prePlaylist: PrePlaylist) { fun link(vertex: PlaylistVertex) {
vertices.add(PlaylistVertex(prePlaylist)) for ((pointer, songVertex) in pointerMap) {
vertex.pointerMap[pointer]?.forEach { index -> vertex.songVertices[index] = songVertex }
} }
}
fun link(vertex: SongVertex) {
val pointer = SongPointer.UID(vertex.preSong.uid)
pointerMap[pointer] = vertex
for (playlistVertex in vertices) {
playlistVertex.pointerMap[pointer]?.forEach { index ->
playlistVertex.songVertices[index] = vertex
}
}
}
fun solve(): Collection<PlaylistVertex> = vertices
} }
internal class PlaylistVertex(val prePlaylist: PrePlaylist) { internal class PlaylistVertex(val prePlaylist: PrePlaylist) {
@ -33,7 +49,7 @@ internal class PlaylistVertex(val prePlaylist: PrePlaylist) {
val pointerMap = val pointerMap =
prePlaylist.songPointers prePlaylist.songPointers
.withIndex() .withIndex()
.associateBy { it.value } .groupBy { it.value }
.mapValuesTo(mutableMapOf()) { it.value.index } .mapValuesTo(mutableMapOf()) { indexed -> indexed.value.map { it.index } }
val tag: Any? = null val tag: Any? = null
} }

View file

@ -19,7 +19,6 @@
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.SongPointer
import org.oxycblt.musikr.tag.interpret.PreSong import org.oxycblt.musikr.tag.interpret.PreSong
internal class SongGraph( internal class SongGraph(
@ -28,66 +27,38 @@ internal class SongGraph(
private val genreGraph: GenreGraph, private val genreGraph: GenreGraph,
private val playlistGraph: PlaylistGraph private val playlistGraph: PlaylistGraph
) { ) {
val vertices = mutableMapOf<Music.UID, SongVertex>() private val vertices = mutableMapOf<Music.UID, SongVertex>()
fun add(preSong: PreSong): SongVertex? { fun link(vertex: SongVertex): Boolean {
val preSong = vertex.preSong
val uid = preSong.uid val uid = preSong.uid
if (vertices.containsKey(uid)) { if (vertices.containsKey(uid)) {
return null return false
}
artistGraph.link(vertex)
genreGraph.link(vertex)
albumGraph.link(vertex)
playlistGraph.link(vertex)
vertices[uid] = vertex
return true
} }
val songGenreVertices = preSong.preGenres.map { preGenre -> genreGraph.add(preGenre) } fun solve(): Collection<SongVertex> {
val songArtistVertices = preSong.preArtists.map { preArtist -> artistGraph.add(preArtist) }
val albumVertex = albumGraph.add(preSong.preAlbum)
val songVertex =
SongVertex(
preSong,
albumVertex,
songArtistVertices.toMutableList(),
songGenreVertices.toMutableList())
songVertex.artistGraph.forEach { artistVertex ->
artistVertex.songVertices.add(songVertex)
songGenreVertices.forEach { genreVertex ->
// Mutually link any new genres to the artist
artistVertex.genreGraph.add(genreVertex)
genreVertex.artistGraph.add(artistVertex)
}
}
songVertex.genreGraph.forEach { genreVertex -> genreVertex.songVertices.add(songVertex) }
vertices[uid] = songVertex
return songVertex
}
fun simplify() {
vertices.entries.forEach { entry -> vertices.entries.forEach { entry ->
val vertex = entry.value val vertex = entry.value
vertex.artistGraph = vertex.artistGraph.distinct().toMutableList() vertex.artistVertices = vertex.artistVertices.distinct().toMutableList()
vertex.genreGraph = vertex.genreGraph.distinct().toMutableList() vertex.genreVertices = vertex.genreVertices.distinct().toMutableList()
playlistGraph.vertices.forEach {
val pointer = SongPointer.UID(entry.key)
val index = it.pointerMap[pointer]
if (index != null) {
it.songVertices[index] = vertex
}
}
} }
return vertices.values
} }
} }
internal class SongVertex( internal class SongVertex(
val preSong: PreSong, val preSong: PreSong,
var albumVertex: AlbumVertex,
var artistGraph: MutableList<ArtistVertex>,
var genreGraph: MutableList<GenreVertex>
) : Vertex { ) : Vertex {
var albumVertex: AlbumVertex? = null
var artistVertices = mutableListOf<ArtistVertex>()
var genreVertices = mutableListOf<GenreVertex>()
override var tag: Any? = null override var tag: Any? = null
override fun toString() = "SongVertex(preSong=$preSong)" override fun toString() = "SongVertex(preSong=$preSong)"

View file

@ -53,23 +53,23 @@ private class LibraryFactoryImpl() : LibraryFactory {
playlistInterpreter: PlaylistInterpreter playlistInterpreter: PlaylistInterpreter
): MutableLibrary { ): MutableLibrary {
val songs = val songs =
graph.songVertex.mapTo(mutableSetOf()) { vertex -> graph.songVertices.mapTo(mutableSetOf()) { vertex ->
SongImpl(SongVertexCore(vertex)).also { vertex.tag = it } SongImpl(SongVertexCore(vertex)).also { vertex.tag = it }
} }
val albums = val albums =
graph.albumVertex.mapTo(mutableSetOf()) { vertex -> graph.albumVertices.mapTo(mutableSetOf()) { vertex ->
AlbumImpl(AlbumVertexCore(vertex)).also { vertex.tag = it } AlbumImpl(AlbumVertexCore(vertex)).also { vertex.tag = it }
} }
val artists = val artists =
graph.artistVertex.mapTo(mutableSetOf()) { vertex -> graph.artistVertices.mapTo(mutableSetOf()) { vertex ->
ArtistImpl(ArtistVertexCore(vertex)).also { vertex.tag = it } ArtistImpl(ArtistVertexCore(vertex)).also { vertex.tag = it }
} }
val genres = val genres =
graph.genreVertex.mapTo(mutableSetOf()) { vertex -> graph.genreVertices.mapTo(mutableSetOf()) { vertex ->
GenreImpl(GenreVertexCore(vertex)).also { vertex.tag = it } GenreImpl(GenreVertexCore(vertex)).also { vertex.tag = it }
} }
val playlists = val playlists =
graph.playlistVertex.mapTo(mutableSetOf()) { vertex -> graph.playlistVertices.mapTo(mutableSetOf()) { vertex ->
PlaylistImpl(PlaylistVertexCore(vertex)) PlaylistImpl(PlaylistVertexCore(vertex))
} }
return LibraryImpl( return LibraryImpl(
@ -121,8 +121,8 @@ private class LibraryFactoryImpl() : LibraryFactory {
} }
private companion object { private companion object {
private inline fun <reified T : Music> tag(vertex: Vertex): T { private inline fun <reified T : Music> tag(vertex: Vertex?): T {
val tag = vertex.tag val tag = vertex?.tag
check(tag is T) { "Dead Vertex Detected: $vertex" } check(tag is T) { "Dead Vertex Detected: $vertex" }
return tag return tag
} }

View file

@ -26,6 +26,8 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
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.Storage
@ -78,11 +80,16 @@ private class EvaluateStepImpl(
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.buffer(Channel.UNLIMITED) .buffer(Channel.UNLIMITED)
val graphBuilder = MusicGraph.builder() val graphBuilder = MusicGraph.builder()
// Concurrent addition of playlists and songs could easily
// break the grapher (remember, individual pipeline elements
// are generally unaware of the highly concurrent nature of
// the pipeline), prevent that with a mutex.
val graphLock = Mutex()
val graphBuild = val graphBuild =
merge( merge(
filterFlow.manager, filterFlow.manager,
preSongs.onEach { graphBuilder.add(it) }, preSongs.onEach { graphLock.withLock { graphBuilder.add(it) } },
prePlaylists.onEach { graphBuilder.add(it) }) prePlaylists.onEach { graphLock.withLock { graphBuilder.add(it) } })
graphBuild.collect() graphBuild.collect()
logger.d("starting graph build") logger.d("starting graph build")
val graph = graphBuilder.build() val graph = graphBuilder.build()