musikr: break apart graph
This commit is contained in:
parent
0f034255af
commit
3a429c14be
7 changed files with 482 additions and 320 deletions
116
musikr/src/main/java/org/oxycblt/musikr/graph/AlbumGraph.kt
Normal file
116
musikr/src/main/java/org/oxycblt/musikr/graph/AlbumGraph.kt
Normal 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)"
|
||||
}
|
115
musikr/src/main/java/org/oxycblt/musikr/graph/ArtistGraph.kt
Normal file
115
musikr/src/main/java/org/oxycblt/musikr/graph/ArtistGraph.kt
Normal 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)"
|
||||
}
|
79
musikr/src/main/java/org/oxycblt/musikr/graph/GenreGraph.kt
Normal file
79
musikr/src/main/java/org/oxycblt/musikr/graph/GenreGraph.kt
Normal 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)"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
94
musikr/src/main/java/org/oxycblt/musikr/graph/SongGraph.kt
Normal file
94
musikr/src/main/java/org/oxycblt/musikr/graph/SongGraph.kt
Normal 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)"
|
||||
}
|
23
musikr/src/main/java/org/oxycblt/musikr/graph/Vertex.kt
Normal file
23
musikr/src/main/java/org/oxycblt/musikr/graph/Vertex.kt
Normal 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?
|
||||
}
|
Loading…
Reference in a new issue