diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/AlbumGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/AlbumGraph.kt
new file mode 100644
index 000000000..fe157fef2
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/graph/AlbumGraph.kt
@@ -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 .
+ */
+
+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()
+
+ 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) {
+ 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) {
+ // 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) :
+ Vertex {
+ val songVertices = mutableSetOf()
+ override var tag: Any? = null
+
+ override fun toString() = "AlbumVertex(preAlbum=$preAlbum)"
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/ArtistGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/ArtistGraph.kt
new file mode 100644
index 000000000..2ca7872df
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/graph/ArtistGraph.kt
@@ -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 .
+ */
+
+package org.oxycblt.musikr.graph
+
+import org.oxycblt.musikr.tag.interpret.PreArtist
+import org.oxycblt.musikr.util.unlikelyToBeNull
+
+internal class ArtistGraph() {
+ val vertices = mutableMapOf()
+
+ 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) {
+ 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) {
+ 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()
+ val albumGraph = mutableSetOf()
+ val genreGraph = mutableSetOf()
+ override var tag: Any? = null
+
+ override fun toString() = "ArtistVertex(preArtist=$preArtist)"
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/GenreGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/GenreGraph.kt
new file mode 100644
index 000000000..3dcb34ad8
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/graph/GenreGraph.kt
@@ -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 .
+ */
+
+package org.oxycblt.musikr.graph
+
+import org.oxycblt.musikr.tag.interpret.PreGenre
+
+internal class GenreGraph {
+ val vertices = mutableMapOf()
+
+ 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) {
+ 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()
+ val artistGraph = mutableSetOf()
+ override var tag: Any? = null
+
+ override fun toString() = "GenreVertex(preGenre=$preGenre)"
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt
index 1b7b2f910..c39e00b42 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt
@@ -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,
@@ -48,332 +42,34 @@ internal data class MusicGraph(
}
private class MusicGraphBuilderImpl : MusicGraph.Builder {
- private val songVertices = mutableMapOf()
- private val albumVertices = mutableMapOf()
- private val artistVertices = mutableMapOf()
- private val genreVertices = mutableMapOf()
- private val playlistVertices = mutableSetOf()
+ 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) {
- 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) {
- 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) {
- 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) {
- 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) {
- // 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,
- var genreVertices: MutableList
-) : Vertex {
- override var tag: Any? = null
-
- override fun toString() = "SongVertex(preSong=$preSong)"
-}
-
-internal class AlbumVertex(val preAlbum: PreAlbum, var artistVertices: MutableList) :
- Vertex {
- val songVertices = mutableSetOf()
- override var tag: Any? = null
-
- override fun toString() = "AlbumVertex(preAlbum=$preAlbum)"
-}
-
-internal class ArtistVertex(
- val preArtist: PreArtist,
-) : Vertex {
- val songVertices = mutableSetOf()
- val albumVertices = mutableSetOf()
- val genreVertices = mutableSetOf()
- override var tag: Any? = null
-
- override fun toString() = "ArtistVertex(preArtist=$preArtist)"
-}
-
-internal class GenreVertex(val preGenre: PreGenre) : Vertex {
- val songVertices = mutableSetOf()
- val artistVertices = mutableSetOf()
- override var tag: Any? = null
-
- override fun toString() = "GenreVertex(preGenre=$preGenre)"
-}
-
-internal class PlaylistVertex(val prePlaylist: PrePlaylist) {
- val songVertices = Array(prePlaylist.songPointers.size) { null }
- val pointerMap =
- prePlaylist.songPointers
- .withIndex()
- .associateBy { it.value }
- .mapValuesTo(mutableMapOf()) { it.value.index }
- val tag: Any? = null
}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/PlaylistGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/PlaylistGraph.kt
new file mode 100644
index 000000000..6c398ec1e
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/graph/PlaylistGraph.kt
@@ -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 .
+ */
+
+package org.oxycblt.musikr.graph
+
+import org.oxycblt.musikr.playlist.interpret.PrePlaylist
+
+internal class PlaylistGraph {
+ val vertices = mutableSetOf()
+
+ fun add(prePlaylist: PrePlaylist) {
+ vertices.add(PlaylistVertex(prePlaylist))
+ }
+}
+
+internal class PlaylistVertex(val prePlaylist: PrePlaylist) {
+ val songVertices = Array(prePlaylist.songPointers.size) { null }
+ val pointerMap =
+ prePlaylist.songPointers
+ .withIndex()
+ .associateBy { it.value }
+ .mapValuesTo(mutableMapOf()) { it.value.index }
+ val tag: Any? = null
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/SongGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/SongGraph.kt
new file mode 100644
index 000000000..638f8fbf3
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/graph/SongGraph.kt
@@ -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 .
+ */
+
+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()
+
+ 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,
+ var genreGraph: MutableList
+) : Vertex {
+ override var tag: Any? = null
+
+ override fun toString() = "SongVertex(preSong=$preSong)"
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/Vertex.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/Vertex.kt
new file mode 100644
index 000000000..889df359e
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/graph/Vertex.kt
@@ -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 .
+ */
+
+package org.oxycblt.musikr.graph
+
+internal interface Vertex {
+ val tag: Any?
+}