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? +}