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