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
import org.oxycblt.musikr.Music
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.PreArtist
import org.oxycblt.musikr.tag.interpret.PreGenre
import org.oxycblt.musikr.tag.interpret.PreSong
import org.oxycblt.musikr.util.unlikelyToBeNull
internal data class MusicGraph(
val songVertex: List<SongVertex>,
@ -48,332 +42,34 @@ internal data class MusicGraph(
}
private class MusicGraphBuilderImpl : MusicGraph.Builder {
private val songVertices = mutableMapOf<Music.UID, SongVertex>()
private val albumVertices = mutableMapOf<PreAlbum, AlbumVertex>()
private val artistVertices = mutableMapOf<PreArtist, ArtistVertex>()
private val genreVertices = mutableMapOf<PreGenre, GenreVertex>()
private val playlistVertices = mutableSetOf<PlaylistVertex>()
private val genreGraph = GenreGraph()
private val artistGraph = ArtistGraph()
private val albumGraph = AlbumGraph(artistGraph)
private val playlistGraph = PlaylistGraph()
private val songVertices = SongGraph(albumGraph, artistGraph, genreGraph, playlistGraph)
override fun add(preSong: PreSong) {
val uid = preSong.uid
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
songVertices.add(preSong)
}
override fun add(prePlaylist: PrePlaylist) {
playlistVertices.add(PlaylistVertex(prePlaylist))
playlistGraph.add(prePlaylist)
}
override fun build(): MusicGraph {
val genreClusters = genreVertices.values.groupBy { it.preGenre.rawName?.lowercase() }
for (cluster in genreClusters.values) {
simplifyGenreCluster(cluster)
}
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
}
}
}
genreGraph.simplify()
artistGraph.simplify()
albumGraph.simplify()
songVertices.simplify()
val graph =
MusicGraph(
songVertices.values.toList(),
albumVertices.values.toList(),
artistVertices.values.toList(),
genreVertices.values.toList(),
playlistVertices)
songVertices.vertices.values.toList(),
albumGraph.vertices.values.toList(),
artistGraph.vertices.values.toList(),
genreGraph.vertices.values.toList(),
playlistGraph.vertices)
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?
}