musikr: break apart graph

This commit is contained in:
Alexander Capehart 2025-01-21 21:41:19 -07:00
parent 0f034255af
commit 3a429c14be
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 482 additions and 320 deletions

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2025 Auxio Project
* AlbumGraph.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.graph
import org.oxycblt.musikr.tag.interpret.PreAlbum
import org.oxycblt.musikr.util.unlikelyToBeNull
internal class AlbumGraph(private val artistGraph: ArtistGraph) {
val vertices = mutableMapOf<PreAlbum, AlbumVertex>()
fun add(preAlbum: PreAlbum): AlbumVertex {
// Albums themselves have their own parent artists that also need to be
// linked up.
val albumartistGraph = preAlbum.preArtists.map { preArtist -> artistGraph.add(preArtist) }
val albumVertex = AlbumVertex(preAlbum, albumartistGraph.toMutableList())
// Album vertex is linked, now link artists back to album.
albumartistGraph.forEach { artistVertex -> artistVertex.albumGraph.add(albumVertex) }
return albumVertex
}
fun simplify() {
val albumClusters = vertices.values.groupBy { it.preAlbum.rawName?.lowercase() }
for (cluster in albumClusters.values) {
simplifyAlbumCluster(cluster)
}
// Remove any edges that wound up connecting to the same artist or genre
// in the end after simplification.
vertices.values.forEach { it.artistGraph = it.artistGraph.distinct().toMutableList() }
}
private fun simplifyAlbumCluster(cluster: Collection<AlbumVertex>) {
if (cluster.size == 1) {
// Nothing to do.
return
}
val fullMusicBrainzIdCoverage = cluster.all { it.preAlbum.musicBrainzId != null }
if (fullMusicBrainzIdCoverage) {
// All albums have MBIDs, nothing needs to be merged.
val mbidClusters = cluster.groupBy { unlikelyToBeNull(it.preAlbum.musicBrainzId) }
for (mbidCluster in mbidClusters.values) {
simplifyAlbumClusterImpl(mbidCluster)
}
return
}
// No full MBID coverage, discard the MBIDs from the graph.
val strippedCluster =
cluster.map {
val noMbidPreAlbum = it.preAlbum.copy(musicBrainzId = null)
val simpleMbidVertex =
vertices.getOrPut(noMbidPreAlbum) {
AlbumVertex(noMbidPreAlbum, it.artistGraph.toMutableList())
}
meldAlbumGraph(it, simpleMbidVertex)
simpleMbidVertex
}
simplifyAlbumClusterImpl(strippedCluster)
}
private fun simplifyAlbumClusterImpl(cluster: Collection<AlbumVertex>) {
// All of these albums are semantically equivalent. Pick the most popular variation
// and merge all the others into it.
if (cluster.size == 1) {
// Nothing to do.
return
}
val clusterSet = cluster.toMutableSet()
val dst = clusterSet.maxBy { it.songVertices.size }
clusterSet.remove(dst)
for (src in clusterSet) {
meldAlbumGraph(src, dst)
}
}
private fun meldAlbumGraph(src: AlbumVertex, dst: AlbumVertex) {
if (src == dst) {
// Same vertex, do nothing
return
}
// Link all songs and artists from the irrelevant album to the relevant album.
dst.songVertices.addAll(src.songVertices)
dst.artistGraph.addAll(src.artistGraph)
// Update all songs and artists to point to the relevant album.
src.songVertices.forEach { it.albumVertex = dst }
src.artistGraph.forEach {
it.albumGraph.remove(src)
it.albumGraph.add(dst)
}
// Remove the irrelevant album from the graph.
vertices.remove(src.preAlbum)
}
}
internal class AlbumVertex(val preAlbum: PreAlbum, var artistGraph: MutableList<ArtistVertex>) :
Vertex {
val songVertices = mutableSetOf<SongVertex>()
override var tag: Any? = null
override fun toString() = "AlbumVertex(preAlbum=$preAlbum)"
}

View file

@ -0,0 +1,115 @@
/*
* Copyright (c) 2025 Auxio Project
* ArtistGraph.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.graph
import org.oxycblt.musikr.tag.interpret.PreArtist
import org.oxycblt.musikr.util.unlikelyToBeNull
internal class ArtistGraph() {
val vertices = mutableMapOf<PreArtist, ArtistVertex>()
fun add(preArtist: PreArtist): ArtistVertex =
vertices.getOrPut(preArtist) { ArtistVertex(preArtist) }
fun simplify() {
val artistClusters = vertices.values.groupBy { it.preArtist.rawName?.lowercase() }
for (cluster in artistClusters.values) {
simplifyArtistCluster(cluster)
}
}
private fun simplifyArtistCluster(cluster: Collection<ArtistVertex>) {
if (cluster.size == 1) {
// Nothing to do.
return
}
val fullMusicBrainzIdCoverage = cluster.all { it.preArtist.musicBrainzId != null }
if (fullMusicBrainzIdCoverage) {
// All artists have MBIDs, nothing needs to be merged.
val mbidClusters = cluster.groupBy { unlikelyToBeNull(it.preArtist.musicBrainzId) }
for (mbidCluster in mbidClusters.values) {
simplifyArtistClusterImpl(mbidCluster)
}
return
}
// No full MBID coverage, discard the MBIDs from the graph.
val strippedCluster =
cluster.map {
val noMbidPreArtist = it.preArtist.copy(musicBrainzId = null)
val simpleMbidVertex =
vertices.getOrPut(noMbidPreArtist) { ArtistVertex(noMbidPreArtist) }
meldArtistGraph(it, simpleMbidVertex)
simpleMbidVertex
}
simplifyArtistClusterImpl(strippedCluster)
}
private fun simplifyArtistClusterImpl(cluster: Collection<ArtistVertex>) {
if (cluster.size == 1) {
// One canonical artist, nothing to collapse
return
}
val clusterSet = cluster.toMutableSet()
val relevantArtistVertex = clusterSet.maxBy { it.songVertices.size }
clusterSet.remove(relevantArtistVertex)
for (irrelevantArtistVertex in clusterSet) {
meldArtistGraph(irrelevantArtistVertex, relevantArtistVertex)
}
}
private fun meldArtistGraph(src: ArtistVertex, dst: ArtistVertex) {
if (src == dst) {
// Same vertex, do nothing
return
}
// Link all songs and albums from the irrelevant artist to the relevant artist.
dst.songVertices.addAll(src.songVertices)
dst.albumGraph.addAll(src.albumGraph)
dst.genreGraph.addAll(src.genreGraph)
// Update all songs, albums, and genres to point to the relevant artist.
src.songVertices.forEach {
val index = it.artistGraph.indexOf(src)
check(index >= 0) { "Illegal state: directed edge between artist and song" }
it.artistGraph[index] = dst
}
src.albumGraph.forEach {
val index = it.artistGraph.indexOf(src)
check(index >= 0) { "Illegal state: directed edge between artist and album" }
it.artistGraph[index] = dst
}
src.genreGraph.forEach {
it.artistGraph.remove(src)
it.artistGraph.add(dst)
}
// Remove the irrelevant artist from the graph.
vertices.remove(src.preArtist)
}
}
internal class ArtistVertex(
val preArtist: PreArtist,
) : Vertex {
val songVertices = mutableSetOf<SongVertex>()
val albumGraph = mutableSetOf<AlbumVertex>()
val genreGraph = mutableSetOf<GenreVertex>()
override var tag: Any? = null
override fun toString() = "ArtistVertex(preArtist=$preArtist)"
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2025 Auxio Project
* GenreGraph.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.graph
import org.oxycblt.musikr.tag.interpret.PreGenre
internal class GenreGraph {
val vertices = mutableMapOf<PreGenre, GenreVertex>()
fun add(preGenre: PreGenre): GenreVertex = vertices.getOrPut(preGenre) { GenreVertex(preGenre) }
fun simplify() {
val genreClusters = vertices.values.groupBy { it.preGenre.rawName?.lowercase() }
for (cluster in genreClusters.values) {
simplifyGenreCluster(cluster)
}
}
private fun simplifyGenreCluster(cluster: Collection<GenreVertex>) {
if (cluster.size == 1) {
// Nothing to do.
return
}
// All of these genres are semantically equivalent. Pick the most popular variation
// and merge all the others into it.
val clusterSet = cluster.toMutableSet()
val dst = clusterSet.maxBy { it.songVertices.size }
clusterSet.remove(dst)
for (src in clusterSet) {
meldGenreGraph(src, dst)
}
}
private fun meldGenreGraph(src: GenreVertex, dst: GenreVertex) {
if (src == dst) {
// Same vertex, do nothing
return
}
// Link all songs and artists from the irrelevant genre to the relevant genre.
dst.songVertices.addAll(src.songVertices)
dst.artistGraph.addAll(src.artistGraph)
// Update all songs and artists to point to the relevant genre.
src.songVertices.forEach {
val index = it.genreGraph.indexOf(src)
check(index >= 0) { "Illegal state: directed edge between genre and song" }
it.genreGraph[index] = dst
}
src.artistGraph.forEach {
it.genreGraph.remove(src)
it.genreGraph.add(dst)
}
// Remove the irrelevant genre from the graph.
vertices.remove(src.preGenre)
}
}
internal class GenreVertex(val preGenre: PreGenre) : Vertex {
val songVertices = mutableSetOf<SongVertex>()
val artistGraph = mutableSetOf<ArtistVertex>()
override var tag: Any? = null
override fun toString() = "GenreVertex(preGenre=$preGenre)"
}

View file

@ -18,14 +18,8 @@
package org.oxycblt.musikr.graph package org.oxycblt.musikr.graph
import org.oxycblt.musikr.Music
import org.oxycblt.musikr.playlist.SongPointer
import org.oxycblt.musikr.playlist.interpret.PrePlaylist import org.oxycblt.musikr.playlist.interpret.PrePlaylist
import org.oxycblt.musikr.tag.interpret.PreAlbum
import org.oxycblt.musikr.tag.interpret.PreArtist
import org.oxycblt.musikr.tag.interpret.PreGenre
import org.oxycblt.musikr.tag.interpret.PreSong import org.oxycblt.musikr.tag.interpret.PreSong
import org.oxycblt.musikr.util.unlikelyToBeNull
internal data class MusicGraph( internal data class MusicGraph(
val songVertex: List<SongVertex>, val songVertex: List<SongVertex>,
@ -48,332 +42,34 @@ internal data class MusicGraph(
} }
private class MusicGraphBuilderImpl : MusicGraph.Builder { private class MusicGraphBuilderImpl : MusicGraph.Builder {
private val songVertices = mutableMapOf<Music.UID, SongVertex>() private val genreGraph = GenreGraph()
private val albumVertices = mutableMapOf<PreAlbum, AlbumVertex>() private val artistGraph = ArtistGraph()
private val artistVertices = mutableMapOf<PreArtist, ArtistVertex>() private val albumGraph = AlbumGraph(artistGraph)
private val genreVertices = mutableMapOf<PreGenre, GenreVertex>() private val playlistGraph = PlaylistGraph()
private val playlistVertices = mutableSetOf<PlaylistVertex>() private val songVertices = SongGraph(albumGraph, artistGraph, genreGraph, playlistGraph)
override fun add(preSong: PreSong) { override fun add(preSong: PreSong) {
val uid = preSong.uid songVertices.add(preSong)
if (songVertices.containsKey(uid)) {
return
}
val songGenreVertices =
preSong.preGenres.map { preGenre ->
genreVertices.getOrPut(preGenre) { GenreVertex(preGenre) }
}
val songArtistVertices =
preSong.preArtists.map { preArtist ->
artistVertices.getOrPut(preArtist) { ArtistVertex(preArtist) }
}
val albumVertex =
albumVertices.getOrPut(preSong.preAlbum) {
// Albums themselves have their own parent artists that also need to be
// linked up.
val albumArtistVertices =
preSong.preAlbum.preArtists.map { preArtist ->
artistVertices.getOrPut(preArtist) { ArtistVertex(preArtist) }
}
val albumVertex = AlbumVertex(preSong.preAlbum, albumArtistVertices.toMutableList())
// Album vertex is linked, now link artists back to album.
albumArtistVertices.forEach { artistVertex ->
artistVertex.albumVertices.add(albumVertex)
}
albumVertex
}
val songVertex =
SongVertex(
preSong,
albumVertex,
songArtistVertices.toMutableList(),
songGenreVertices.toMutableList())
albumVertex.songVertices.add(songVertex)
songArtistVertices.forEach { artistVertex ->
artistVertex.songVertices.add(songVertex)
songGenreVertices.forEach { genreVertex ->
// Mutually link any new genres to the artist
artistVertex.genreVertices.add(genreVertex)
genreVertex.artistVertices.add(artistVertex)
}
}
songGenreVertices.forEach { genreVertex -> genreVertex.songVertices.add(songVertex) }
songVertices[uid] = songVertex
} }
override fun add(prePlaylist: PrePlaylist) { override fun add(prePlaylist: PrePlaylist) {
playlistVertices.add(PlaylistVertex(prePlaylist)) playlistGraph.add(prePlaylist)
} }
override fun build(): MusicGraph { override fun build(): MusicGraph {
val genreClusters = genreVertices.values.groupBy { it.preGenre.rawName?.lowercase() } genreGraph.simplify()
for (cluster in genreClusters.values) { artistGraph.simplify()
simplifyGenreCluster(cluster) albumGraph.simplify()
} songVertices.simplify()
val artistClusters = artistVertices.values.groupBy { it.preArtist.rawName?.lowercase() }
for (cluster in artistClusters.values) {
simplifyArtistCluster(cluster)
}
val albumClusters = albumVertices.values.groupBy { it.preAlbum.rawName?.lowercase() }
for (cluster in albumClusters.values) {
simplifyAlbumCluster(cluster)
}
// Remove any edges that wound up connecting to the same artist or genre
// in the end after simplification.
albumVertices.values.forEach {
it.artistVertices = it.artistVertices.distinct().toMutableList()
}
songVertices.entries.forEach { entry ->
val vertex = entry.value
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
}
}
}
val graph = val graph =
MusicGraph( MusicGraph(
songVertices.values.toList(), songVertices.vertices.values.toList(),
albumVertices.values.toList(), albumGraph.vertices.values.toList(),
artistVertices.values.toList(), artistGraph.vertices.values.toList(),
genreVertices.values.toList(), genreGraph.vertices.values.toList(),
playlistVertices) playlistGraph.vertices)
return graph return graph
} }
private fun simplifyGenreCluster(cluster: Collection<GenreVertex>) {
if (cluster.size == 1) {
// Nothing to do.
return
}
// All of these genres are semantically equivalent. Pick the most popular variation
// and merge all the others into it.
val clusterSet = cluster.toMutableSet()
val dst = clusterSet.maxBy { it.songVertices.size }
clusterSet.remove(dst)
for (src in clusterSet) {
meldGenreVertices(src, dst)
}
}
private fun meldGenreVertices(src: GenreVertex, dst: GenreVertex) {
if (src == dst) {
// Same vertex, do nothing
return
}
// Link all songs and artists from the irrelevant genre to the relevant genre.
dst.songVertices.addAll(src.songVertices)
dst.artistVertices.addAll(src.artistVertices)
// Update all songs and artists to point to the relevant genre.
src.songVertices.forEach {
val index = it.genreVertices.indexOf(src)
check(index >= 0) { "Illegal state: directed edge between genre and song" }
it.genreVertices[index] = dst
}
src.artistVertices.forEach {
it.genreVertices.remove(src)
it.genreVertices.add(dst)
}
// Remove the irrelevant genre from the graph.
genreVertices.remove(src.preGenre)
}
private fun simplifyArtistCluster(cluster: Collection<ArtistVertex>) {
if (cluster.size == 1) {
// Nothing to do.
return
}
val fullMusicBrainzIdCoverage = cluster.all { it.preArtist.musicBrainzId != null }
if (fullMusicBrainzIdCoverage) {
// All artists have MBIDs, nothing needs to be merged.
val mbidClusters = cluster.groupBy { unlikelyToBeNull(it.preArtist.musicBrainzId) }
for (mbidCluster in mbidClusters.values) {
simplifyArtistClusterImpl(mbidCluster)
}
return
}
// No full MBID coverage, discard the MBIDs from the graph.
val strippedCluster =
cluster.map {
val noMbidPreArtist = it.preArtist.copy(musicBrainzId = null)
val simpleMbidVertex =
artistVertices.getOrPut(noMbidPreArtist) { ArtistVertex(noMbidPreArtist) }
meldArtistVertices(it, simpleMbidVertex)
simpleMbidVertex
}
simplifyArtistClusterImpl(strippedCluster)
}
private fun simplifyArtistClusterImpl(cluster: Collection<ArtistVertex>) {
if (cluster.size == 1) {
// One canonical artist, nothing to collapse
return
}
val clusterSet = cluster.toMutableSet()
val relevantArtistVertex = clusterSet.maxBy { it.songVertices.size }
clusterSet.remove(relevantArtistVertex)
for (irrelevantArtistVertex in clusterSet) {
meldArtistVertices(irrelevantArtistVertex, relevantArtistVertex)
}
}
private fun meldArtistVertices(src: ArtistVertex, dst: ArtistVertex) {
if (src == dst) {
// Same vertex, do nothing
return
}
// Link all songs and albums from the irrelevant artist to the relevant artist.
dst.songVertices.addAll(src.songVertices)
dst.albumVertices.addAll(src.albumVertices)
dst.genreVertices.addAll(src.genreVertices)
// Update all songs, albums, and genres to point to the relevant artist.
src.songVertices.forEach {
val index = it.artistVertices.indexOf(src)
check(index >= 0) { "Illegal state: directed edge between artist and song" }
it.artistVertices[index] = dst
}
src.albumVertices.forEach {
val index = it.artistVertices.indexOf(src)
check(index >= 0) { "Illegal state: directed edge between artist and album" }
it.artistVertices[index] = dst
}
src.genreVertices.forEach {
it.artistVertices.remove(src)
it.artistVertices.add(dst)
}
// Remove the irrelevant artist from the graph.
artistVertices.remove(src.preArtist)
}
private fun simplifyAlbumCluster(cluster: Collection<AlbumVertex>) {
if (cluster.size == 1) {
// Nothing to do.
return
}
val fullMusicBrainzIdCoverage = cluster.all { it.preAlbum.musicBrainzId != null }
if (fullMusicBrainzIdCoverage) {
// All albums have MBIDs, nothing needs to be merged.
val mbidClusters = cluster.groupBy { unlikelyToBeNull(it.preAlbum.musicBrainzId) }
for (mbidCluster in mbidClusters.values) {
simplifyAlbumClusterImpl(mbidCluster)
}
return
}
// No full MBID coverage, discard the MBIDs from the graph.
val strippedCluster =
cluster.map {
val noMbidPreAlbum = it.preAlbum.copy(musicBrainzId = null)
val simpleMbidVertex =
albumVertices.getOrPut(noMbidPreAlbum) {
AlbumVertex(noMbidPreAlbum, it.artistVertices.toMutableList())
}
meldAlbumVertices(it, simpleMbidVertex)
simpleMbidVertex
}
simplifyAlbumClusterImpl(strippedCluster)
}
private fun simplifyAlbumClusterImpl(cluster: Collection<AlbumVertex>) {
// All of these albums are semantically equivalent. Pick the most popular variation
// and merge all the others into it.
if (cluster.size == 1) {
// Nothing to do.
return
}
val clusterSet = cluster.toMutableSet()
val dst = clusterSet.maxBy { it.songVertices.size }
clusterSet.remove(dst)
for (src in clusterSet) {
meldAlbumVertices(src, dst)
}
}
private fun meldAlbumVertices(src: AlbumVertex, dst: AlbumVertex) {
if (src == dst) {
// Same vertex, do nothing
return
}
// Link all songs and artists from the irrelevant album to the relevant album.
dst.songVertices.addAll(src.songVertices)
dst.artistVertices.addAll(src.artistVertices)
// Update all songs and artists to point to the relevant album.
src.songVertices.forEach { it.albumVertex = dst }
src.artistVertices.forEach {
it.albumVertices.remove(src)
it.albumVertices.add(dst)
}
// Remove the irrelevant album from the graph.
albumVertices.remove(src.preAlbum)
}
}
internal interface Vertex {
val tag: Any?
}
internal class SongVertex(
val preSong: PreSong,
var albumVertex: AlbumVertex,
var artistVertices: MutableList<ArtistVertex>,
var genreVertices: MutableList<GenreVertex>
) : Vertex {
override var tag: Any? = null
override fun toString() = "SongVertex(preSong=$preSong)"
}
internal class AlbumVertex(val preAlbum: PreAlbum, var artistVertices: MutableList<ArtistVertex>) :
Vertex {
val songVertices = mutableSetOf<SongVertex>()
override var tag: Any? = null
override fun toString() = "AlbumVertex(preAlbum=$preAlbum)"
}
internal class ArtistVertex(
val preArtist: PreArtist,
) : Vertex {
val songVertices = mutableSetOf<SongVertex>()
val albumVertices = mutableSetOf<AlbumVertex>()
val genreVertices = mutableSetOf<GenreVertex>()
override var tag: Any? = null
override fun toString() = "ArtistVertex(preArtist=$preArtist)"
}
internal class GenreVertex(val preGenre: PreGenre) : Vertex {
val songVertices = mutableSetOf<SongVertex>()
val artistVertices = mutableSetOf<ArtistVertex>()
override var tag: Any? = null
override fun toString() = "GenreVertex(preGenre=$preGenre)"
}
internal class PlaylistVertex(val prePlaylist: PrePlaylist) {
val songVertices = Array<SongVertex?>(prePlaylist.songPointers.size) { null }
val pointerMap =
prePlaylist.songPointers
.withIndex()
.associateBy { it.value }
.mapValuesTo(mutableMapOf()) { it.value.index }
val tag: Any? = null
} }

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Auxio Project
* PlaylistGraph.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.graph
import org.oxycblt.musikr.playlist.interpret.PrePlaylist
internal class PlaylistGraph {
val vertices = mutableSetOf<PlaylistVertex>()
fun add(prePlaylist: PrePlaylist) {
vertices.add(PlaylistVertex(prePlaylist))
}
}
internal class PlaylistVertex(val prePlaylist: PrePlaylist) {
val songVertices = Array<SongVertex?>(prePlaylist.songPointers.size) { null }
val pointerMap =
prePlaylist.songPointers
.withIndex()
.associateBy { it.value }
.mapValuesTo(mutableMapOf()) { it.value.index }
val tag: Any? = null
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2025 Auxio Project
* SongGraph.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.graph
import org.oxycblt.musikr.Music
import org.oxycblt.musikr.playlist.SongPointer
import org.oxycblt.musikr.tag.interpret.PreSong
internal class SongGraph(
private val albumGraph: AlbumGraph,
private val artistGraph: ArtistGraph,
private val genreGraph: GenreGraph,
private val playlistGraph: PlaylistGraph
) {
val vertices = mutableMapOf<Music.UID, SongVertex>()
fun add(preSong: PreSong): SongVertex? {
val uid = preSong.uid
if (vertices.containsKey(uid)) {
return null
}
val songGenreVertices = preSong.preGenres.map { preGenre -> genreGraph.add(preGenre) }
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 ->
val vertex = entry.value
vertex.artistGraph = vertex.artistGraph.distinct().toMutableList()
vertex.genreGraph = vertex.genreGraph.distinct().toMutableList()
playlistGraph.vertices.forEach {
val pointer = SongPointer.UID(entry.key)
val index = it.pointerMap[pointer]
if (index != null) {
it.songVertices[index] = vertex
}
}
}
}
}
internal class SongVertex(
val preSong: PreSong,
var albumVertex: AlbumVertex,
var artistGraph: MutableList<ArtistVertex>,
var genreGraph: MutableList<GenreVertex>
) : Vertex {
override var tag: Any? = null
override fun toString() = "SongVertex(preSong=$preSong)"
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Auxio Project
* Vertex.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.graph
internal interface Vertex {
val tag: Any?
}