musikr: introduce new graphing system
This does all the required simpification steps as before, but now creates mutual edges between parent and child items that removes the finicky finalization logic in models.
This commit is contained in:
parent
7f7ee94f45
commit
970fdb2a8d
12 changed files with 508 additions and 543 deletions
|
@ -27,7 +27,7 @@ import kotlinx.coroutines.yield
|
|||
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
|
||||
import org.oxycblt.auxio.musikr.Indexer
|
||||
import org.oxycblt.auxio.musikr.IndexingProgress
|
||||
import org.oxycblt.auxio.musikr.model.impl.MutableLibrary
|
||||
import org.oxycblt.auxio.musikr.model.MutableLibrary
|
||||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.Separators
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* PlaylistLinker.kt is part of Auxio.
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* GraphModule.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
|
||||
|
@ -16,18 +16,15 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.model.graph
|
||||
package org.oxycblt.auxio.musikr.graph
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.oxycblt.auxio.musikr.model.impl.PlaylistImpl
|
||||
import org.oxycblt.auxio.musikr.playlist.PlaylistFile
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
class PlaylistLinker {
|
||||
fun register(
|
||||
playlists: Flow<PlaylistFile>,
|
||||
linkedSongs: Flow<AlbumLinker.LinkedSong>
|
||||
): Flow<LinkedPlaylist> = emptyFlow()
|
||||
|
||||
fun resolve(): Collection<PlaylistImpl> = setOf()
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface GraphModule {
|
||||
@Binds fun musicGraphFactory(interpreter: MusicGraphFactoryImpl): MusicGraph.Factory
|
||||
}
|
306
app/src/main/java/org/oxycblt/auxio/musikr/graph/MusicGraph.kt
Normal file
306
app/src/main/java/org/oxycblt/auxio/musikr/graph/MusicGraph.kt
Normal file
|
@ -0,0 +1,306 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* MusicGraph.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.auxio.musikr.graph
|
||||
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreAlbum
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreArtist
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreGenre
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreSong
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
import timber.log.Timber as L
|
||||
|
||||
data class MusicGraph(
|
||||
val songVertex: List<SongVertex>,
|
||||
val albumVertex: List<AlbumVertex>,
|
||||
val artistVertex: List<ArtistVertex>,
|
||||
val genreVertex: List<GenreVertex>
|
||||
) {
|
||||
interface Builder {
|
||||
fun add(preSong: PreSong)
|
||||
|
||||
fun build(): MusicGraph
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun builder(): Builder
|
||||
}
|
||||
}
|
||||
|
||||
class MusicGraphFactoryImpl @Inject constructor() : MusicGraph.Factory {
|
||||
override fun builder(): MusicGraph.Builder = MusicGraphBuilderImpl()
|
||||
}
|
||||
|
||||
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>()
|
||||
|
||||
override fun add(preSong: PreSong) {
|
||||
val uid = preSong.computeUid()
|
||||
if (songVertices.containsKey(uid)) {
|
||||
L.d("Song ${preSong.path} already in graph at ${songVertices[uid]?.preSong?.path}")
|
||||
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 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.values.forEach {
|
||||
it.artistVertices = it.artistVertices.distinct().toMutableList()
|
||||
it.genreVertices = it.genreVertices.distinct().toMutableList()
|
||||
}
|
||||
|
||||
return MusicGraph(
|
||||
songVertices.values.toList(),
|
||||
albumVertices.values.toList(),
|
||||
artistVertices.values.toList(),
|
||||
genreVertices.values.toList())
|
||||
}
|
||||
|
||||
private fun simplifyGenreCluster(cluster: Collection<GenreVertex>) {
|
||||
// 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) {
|
||||
// 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>) {
|
||||
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) {
|
||||
// Link all songs and albums from the irrelevant artist to the relevant artist.
|
||||
dst.songVertices.addAll(src.songVertices)
|
||||
dst.albumVertices.addAll(src.albumVertices)
|
||||
// Update all songs and albums 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
|
||||
}
|
||||
// Remove the irrelevant artist from the graph.
|
||||
artistVertices.remove(src.preArtist)
|
||||
}
|
||||
|
||||
private fun simplifyAlbumCluster(cluster: Collection<AlbumVertex>) {
|
||||
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.
|
||||
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) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
class SongVertex(
|
||||
val preSong: PreSong,
|
||||
var albumVertex: AlbumVertex,
|
||||
var artistVertices: MutableList<ArtistVertex>,
|
||||
var genreVertices: MutableList<GenreVertex>
|
||||
) {
|
||||
var tag: Any? = null
|
||||
}
|
||||
|
||||
class AlbumVertex(val preAlbum: PreAlbum, var artistVertices: MutableList<ArtistVertex>) {
|
||||
val songVertices = mutableSetOf<SongVertex>()
|
||||
var tag: Any? = null
|
||||
}
|
||||
|
||||
class ArtistVertex(
|
||||
val preArtist: PreArtist,
|
||||
) {
|
||||
val songVertices = mutableSetOf<SongVertex>()
|
||||
val albumVertices = mutableSetOf<AlbumVertex>()
|
||||
val genreVertices = mutableSetOf<GenreVertex>()
|
||||
var tag: Any? = null
|
||||
}
|
||||
|
||||
class GenreVertex(val preGenre: PreGenre) {
|
||||
val songVertices = mutableSetOf<SongVertex>()
|
||||
val artistVertices = mutableSetOf<ArtistVertex>()
|
||||
var tag: Any? = null
|
||||
}
|
|
@ -16,10 +16,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.model.impl
|
||||
package org.oxycblt.auxio.musikr.model
|
||||
|
||||
import kotlin.math.min
|
||||
import org.oxycblt.auxio.list.sort.Sort
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -27,21 +25,30 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.MusicType
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
import org.oxycblt.auxio.musikr.model.graph.LinkedAlbum
|
||||
import org.oxycblt.auxio.musikr.model.graph.LinkedSong
|
||||
import org.oxycblt.auxio.musikr.tag.Date
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreAlbum
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreArtist
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreGenre
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreSong
|
||||
import org.oxycblt.auxio.util.update
|
||||
|
||||
interface SongHandle {
|
||||
val preSong: PreSong
|
||||
|
||||
fun resolveAlbum(): Album
|
||||
|
||||
fun resolveArtists(): List<Artist>
|
||||
|
||||
fun resolveGenres(): List<Genre>
|
||||
}
|
||||
|
||||
/**
|
||||
* Library-backed implementation of [Song].
|
||||
*
|
||||
* @param linkedSong The completed [LinkedSong] all metadata van be inferred from
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class SongImpl(linkedSong: LinkedSong) : Song {
|
||||
private val preSong = linkedSong.preSong
|
||||
class SongImpl(private val handle: SongHandle) : Song {
|
||||
private val preSong = handle.preSong
|
||||
|
||||
override val uid = preSong.computeUid()
|
||||
override val name = preSong.name
|
||||
|
@ -57,9 +64,14 @@ class SongImpl(linkedSong: LinkedSong) : Song {
|
|||
override val lastModified = preSong.lastModified
|
||||
override val dateAdded = preSong.dateAdded
|
||||
override val cover = Cover.single(this)
|
||||
override val album = linkedSong.album.resolve(this)
|
||||
override val artists = linkedSong.artists.resolve(this)
|
||||
override val genres = linkedSong.genres.resolve(this)
|
||||
override val album: Album
|
||||
get() = handle.resolveAlbum()
|
||||
|
||||
override val artists: List<Artist>
|
||||
get() = handle.resolveArtists()
|
||||
|
||||
override val genres: List<Genre>
|
||||
get() = handle.resolveGenres()
|
||||
|
||||
private val hashCode = 31 * uid.hashCode() + preSong.hashCode()
|
||||
|
||||
|
@ -71,13 +83,20 @@ class SongImpl(linkedSong: LinkedSong) : Song {
|
|||
override fun toString() = "Song(uid=$uid, name=$name)"
|
||||
}
|
||||
|
||||
interface AlbumHandle {
|
||||
val preAlbum: PreAlbum
|
||||
val songs: List<Song>
|
||||
|
||||
fun resolveArtists(): List<Artist>
|
||||
}
|
||||
|
||||
/**
|
||||
* Library-backed implementation of [Album].
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class AlbumImpl(linkedAlbum: LinkedAlbum) : Album {
|
||||
private val preAlbum = linkedAlbum.preAlbum
|
||||
class AlbumImpl(private val handle: AlbumHandle) : Album {
|
||||
private val preAlbum = handle.preAlbum
|
||||
|
||||
override val uid =
|
||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||
|
@ -91,41 +110,33 @@ class AlbumImpl(linkedAlbum: LinkedAlbum) : Album {
|
|||
}
|
||||
override val name = preAlbum.name
|
||||
override val releaseType = preAlbum.releaseType
|
||||
override var durationMs = 0L
|
||||
override var dateAdded = 0L
|
||||
override var cover: Cover = Cover.nil()
|
||||
override var dates: Date.Range? = null
|
||||
override val durationMs = handle.songs.sumOf { it.durationMs }
|
||||
override val dateAdded = handle.songs.minOf { it.dateAdded }
|
||||
override val cover = Cover.multi(handle.songs)
|
||||
override val dates: Date.Range? =
|
||||
handle.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) }
|
||||
|
||||
override val artists = linkedAlbum.artists.resolve(this)
|
||||
override val songs = mutableSetOf<Song>()
|
||||
override val artists: List<Artist>
|
||||
get() = handle.resolveArtists()
|
||||
|
||||
private var hashCode = 31 * uid.hashCode() + preAlbum.hashCode()
|
||||
override val songs = handle.songs
|
||||
|
||||
private val hashCode = 31 * (31 * uid.hashCode() + preAlbum.hashCode()) + songs.hashCode()
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
// Since equality on public-facing music models is not identical to the tag equality,
|
||||
// we just compare raw instances and how they are interpreted.
|
||||
override fun equals(other: Any?) =
|
||||
other is AlbumImpl && uid == other.uid && preAlbum == other.preAlbum && songs == other.songs
|
||||
|
||||
override fun toString() = "Album(uid=$uid, name=$name)"
|
||||
}
|
||||
|
||||
fun link(song: SongImpl) {
|
||||
songs.add(song)
|
||||
durationMs += song.durationMs
|
||||
dateAdded = min(dateAdded, song.dateAdded)
|
||||
if (song.date != null) {
|
||||
dates =
|
||||
dates?.let {
|
||||
if (song.date < it.min) Date.Range(song.date, it.max)
|
||||
else if (song.date > it.max) Date.Range(it.min, song.date) else it
|
||||
} ?: Date.Range(song.date, song.date)
|
||||
}
|
||||
}
|
||||
interface ArtistHandle {
|
||||
val preArtist: PreArtist
|
||||
val songs: Set<Song>
|
||||
val albums: Set<Album>
|
||||
|
||||
fun finalize() {
|
||||
cover = Cover.single(Sort(Sort.Mode.ByTrack, Sort.Direction.ASCENDING).songs(songs).first())
|
||||
}
|
||||
fun resolveGenres(): Set<Genre>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,58 +144,41 @@ class AlbumImpl(linkedAlbum: LinkedAlbum) : Album {
|
|||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class ArtistImpl(private val preArtist: PreArtist) : Artist {
|
||||
class ArtistImpl(private val handle: ArtistHandle) : Artist {
|
||||
override val uid =
|
||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||
preArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ARTISTS, it) }
|
||||
?: Music.UID.auxio(MusicType.ARTISTS) { update(preArtist.rawName) }
|
||||
override val name = preArtist.name
|
||||
handle.preArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ARTISTS, it) }
|
||||
?: Music.UID.auxio(MusicType.ARTISTS) { update(handle.preArtist.rawName) }
|
||||
override val name = handle.preArtist.name
|
||||
|
||||
override val songs = mutableSetOf<Song>()
|
||||
override val songs = handle.songs
|
||||
override var explicitAlbums = handle.albums
|
||||
override var implicitAlbums = handle.songs.mapTo(mutableSetOf()) { it.album } - handle.albums
|
||||
|
||||
private val albums = mutableListOf<Album>()
|
||||
override var explicitAlbums = mutableSetOf<Album>()
|
||||
override var implicitAlbums = mutableSetOf<Album>()
|
||||
override val genres: List<Genre>
|
||||
get() = handle.resolveGenres().toList()
|
||||
|
||||
override var genres = listOf<Genre>()
|
||||
override var durationMs = 0L
|
||||
override var cover = Cover.nil()
|
||||
override val durationMs = handle.songs.sumOf { it.durationMs }
|
||||
override val cover = Cover.multi(handle.songs)
|
||||
|
||||
private var hashCode = 31 * uid.hashCode() + preArtist.hashCode()
|
||||
private val hashCode =
|
||||
31 * (31 * uid.hashCode() + handle.preArtist.hashCode()) * handle.songs.hashCode()
|
||||
|
||||
// Note: Append song contents to MusicParent equality so that artists with
|
||||
// the same UID but different songs are not equal.
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
// Since equality on public-facing music models is not identical to the tag equality,
|
||||
// we just compare raw instances and how they are interpreted.
|
||||
override fun equals(other: Any?) =
|
||||
other is ArtistImpl &&
|
||||
uid == other.uid &&
|
||||
preArtist == other.preArtist &&
|
||||
handle.preArtist == other.handle.preArtist &&
|
||||
songs == other.songs
|
||||
|
||||
override fun toString() = "Artist(uid=$uid, name=$name)"
|
||||
}
|
||||
|
||||
fun link(song: SongImpl) {
|
||||
songs.add(song)
|
||||
durationMs += song.durationMs
|
||||
hashCode = 31 * hashCode + song.hashCode()
|
||||
}
|
||||
|
||||
fun link(album: AlbumImpl) {
|
||||
albums.add(album)
|
||||
}
|
||||
|
||||
fun finalize() {
|
||||
explicitAlbums.addAll(albums)
|
||||
implicitAlbums.addAll(songs.mapTo(mutableSetOf()) { it.album } - albums.toSet())
|
||||
cover = Cover.multi(songs)
|
||||
genres =
|
||||
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
|
||||
.genres(songs.flatMapTo(mutableSetOf()) { it.genres })
|
||||
.sortedByDescending { genre -> songs.count { it.genres.contains(genre) } }
|
||||
}
|
||||
interface GenreHandle {
|
||||
val preGenre: PreGenre
|
||||
val songs: Set<Song>
|
||||
val artists: Set<Artist>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -192,32 +186,25 @@ class ArtistImpl(private val preArtist: PreArtist) : Artist {
|
|||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class GenreImpl(private val preGenre: PreGenre) : Genre {
|
||||
override val uid = Music.UID.auxio(MusicType.GENRES) { update(preGenre.rawName) }
|
||||
override val name = preGenre.name
|
||||
class GenreImpl(private val handle: GenreHandle) : Genre {
|
||||
override val uid = Music.UID.auxio(MusicType.GENRES) { update(handle.preGenre.rawName) }
|
||||
override val name = handle.preGenre.name
|
||||
|
||||
override val songs = mutableSetOf<Song>()
|
||||
override val artists = mutableSetOf<Artist>()
|
||||
override var durationMs = 0L
|
||||
override var cover = Cover.nil()
|
||||
override val durationMs = handle.songs.sumOf { it.durationMs }
|
||||
override val cover = Cover.multi(handle.songs)
|
||||
|
||||
private var hashCode = uid.hashCode()
|
||||
private val hashCode =
|
||||
31 * (31 * uid.hashCode() + handle.preGenre.hashCode()) + songs.hashCode()
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is GenreImpl && uid == other.uid && preGenre == other.preGenre && songs == other.songs
|
||||
other is GenreImpl &&
|
||||
uid == other.uid &&
|
||||
handle.preGenre == other.handle.preGenre &&
|
||||
songs == other.songs
|
||||
|
||||
override fun toString() = "Genre(uid=$uid, name=$name)"
|
||||
|
||||
fun link(song: SongImpl) {
|
||||
songs.add(song)
|
||||
durationMs += song.durationMs
|
||||
hashCode = 31 * hashCode + song.hashCode()
|
||||
}
|
||||
|
||||
fun finalize() {
|
||||
cover = Cover.multi(songs)
|
||||
artists.addAll(songs.flatMapTo(mutableSetOf()) { it.artists })
|
||||
}
|
||||
}
|
|
@ -16,13 +16,22 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.model.impl
|
||||
package org.oxycblt.auxio.musikr.model
|
||||
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Library
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.musikr.fs.Path
|
||||
import org.oxycblt.auxio.musikr.graph.AlbumVertex
|
||||
import org.oxycblt.auxio.musikr.graph.ArtistVertex
|
||||
import org.oxycblt.auxio.musikr.graph.GenreVertex
|
||||
import org.oxycblt.auxio.musikr.graph.MusicGraph
|
||||
import org.oxycblt.auxio.musikr.graph.SongVertex
|
||||
|
||||
interface MutableLibrary : Library {
|
||||
suspend fun createPlaylist(name: String, songs: List<Song>): MutableLibrary
|
||||
|
@ -36,6 +45,69 @@ interface MutableLibrary : Library {
|
|||
suspend fun deletePlaylist(playlist: Playlist): MutableLibrary
|
||||
}
|
||||
|
||||
interface LibraryFactory {
|
||||
fun create(graph: MusicGraph): MutableLibrary
|
||||
}
|
||||
|
||||
class LibraryFactoryImpl @Inject constructor() : LibraryFactory {
|
||||
override fun create(graph: MusicGraph): MutableLibrary {
|
||||
val songs =
|
||||
graph.songVertex.mapTo(mutableSetOf()) { vertex ->
|
||||
SongImpl(SongVertexHandle(vertex)).also { vertex.tag = it }
|
||||
}
|
||||
val albums =
|
||||
graph.albumVertex.mapTo(mutableSetOf()) { vertex ->
|
||||
AlbumImpl(AlbumVertexHandle(vertex)).also { vertex.tag = it }
|
||||
}
|
||||
val artists =
|
||||
graph.artistVertex.mapTo(mutableSetOf()) { vertex ->
|
||||
ArtistImpl(ArtistVertexHandle(vertex)).also { vertex.tag = it }
|
||||
}
|
||||
val genres =
|
||||
graph.genreVertex.mapTo(mutableSetOf()) { vertex ->
|
||||
GenreImpl(GenreVertexHandle(vertex)).also { vertex.tag = it }
|
||||
}
|
||||
return LibraryImpl(songs, albums, artists, genres)
|
||||
}
|
||||
|
||||
private class SongVertexHandle(private val vertex: SongVertex) : SongHandle {
|
||||
override val preSong = vertex.preSong
|
||||
|
||||
override fun resolveAlbum() = vertex.albumVertex.tag as Album
|
||||
|
||||
override fun resolveArtists() = vertex.artistVertices.map { it.tag as Artist }
|
||||
|
||||
override fun resolveGenres() = vertex.genreVertices.map { it.tag as Genre }
|
||||
}
|
||||
|
||||
private class AlbumVertexHandle(private val vertex: AlbumVertex) : AlbumHandle {
|
||||
override val preAlbum = vertex.preAlbum
|
||||
|
||||
override val songs = vertex.songVertices.map { SongImpl(SongVertexHandle(it)) }
|
||||
|
||||
override fun resolveArtists() = vertex.artistVertices.map { it.tag as Artist }
|
||||
}
|
||||
|
||||
private class ArtistVertexHandle(private val vertex: ArtistVertex) : ArtistHandle {
|
||||
override val preArtist = vertex.preArtist
|
||||
|
||||
override val songs = vertex.songVertices.mapTo(mutableSetOf()) { it.tag as Song }
|
||||
|
||||
override val albums = vertex.albumVertices.mapTo(mutableSetOf()) { it.tag as Album }
|
||||
|
||||
override fun resolveGenres() =
|
||||
vertex.genreVertices.mapTo(mutableSetOf()) { it.tag as Genre }
|
||||
}
|
||||
|
||||
private class GenreVertexHandle(vertex: GenreVertex) : GenreHandle {
|
||||
override val preGenre = vertex.preGenre
|
||||
|
||||
override val songs = vertex.songVertices.mapTo(mutableSetOf()) { it.tag as Song }
|
||||
|
||||
override val artists = vertex.artistVertices.mapTo(mutableSetOf()) { it.tag as Artist }
|
||||
}
|
||||
}
|
||||
|
||||
class LibraryImpl(
|
||||
override val songs: Collection<SongImpl>,
|
||||
override val albums: Collection<AlbumImpl>,
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* Contribution.kt is part of Auxio.
|
||||
* ModelModule.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
|
||||
|
@ -16,21 +16,15 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.model.graph
|
||||
package org.oxycblt.auxio.musikr.model
|
||||
|
||||
class Contribution<T> {
|
||||
private val map = mutableMapOf<T, Int>()
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
val candidates: Collection<T>
|
||||
get() = map.keys
|
||||
|
||||
fun contribute(key: T) {
|
||||
map[key] = map.getOrDefault(key, 0) + 1
|
||||
}
|
||||
|
||||
fun contribute(keys: Collection<T>) {
|
||||
keys.forEach { contribute(it) }
|
||||
}
|
||||
|
||||
fun resolve() = map.maxByOrNull { it.value }?.key ?: error("Nothing was contributed")
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface ModelModule {
|
||||
@Binds fun libraryFactory(factory: LibraryFactoryImpl): LibraryFactory
|
||||
}
|
|
@ -16,29 +16,33 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.musikr.model.impl
|
||||
package org.oxycblt.auxio.musikr.model
|
||||
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.musikr.cover.Cover
|
||||
import org.oxycblt.auxio.musikr.model.graph.LinkedPlaylist
|
||||
import org.oxycblt.auxio.musikr.playlist.PlaylistHandle
|
||||
import org.oxycblt.auxio.musikr.tag.Name
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PrePlaylist
|
||||
|
||||
class PlaylistImpl(linkedPlaylist: LinkedPlaylist) : Playlist {
|
||||
private val prePlaylist = linkedPlaylist.prePlaylist
|
||||
override val uid = prePlaylist.handle.uid
|
||||
override val name: Name.Known = prePlaylist.name
|
||||
override val songs = linkedPlaylist.songs.resolve(this)
|
||||
override val durationMs = songs.sumOf { it.durationMs }
|
||||
override val cover = Cover.multi(songs)
|
||||
private var hashCode = uid.hashCode()
|
||||
interface PlaylistCore {
|
||||
val prePlaylist: PrePlaylist
|
||||
val handle: PlaylistHandle
|
||||
val songs: List<Song>
|
||||
}
|
||||
|
||||
init {
|
||||
hashCode = 31 * hashCode + prePlaylist.hashCode()
|
||||
hashCode = 31 * hashCode + songs.hashCode()
|
||||
}
|
||||
class PlaylistImpl(private val core: PlaylistCore) : Playlist {
|
||||
override val uid = core.handle.uid
|
||||
override val name: Name.Known = core.prePlaylist.name
|
||||
override val durationMs = core.songs.sumOf { it.durationMs }
|
||||
override val cover = Cover.multi(core.songs)
|
||||
override val songs = core.songs
|
||||
|
||||
private var hashCode =
|
||||
31 * (31 * uid.hashCode() + core.prePlaylist.hashCode()) + songs.hashCode()
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is PlaylistImpl && prePlaylist == other.prePlaylist && songs == other.songs
|
||||
other is PlaylistImpl && core.prePlaylist == other.core.prePlaylist && songs == other.songs
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* AlbumLinker.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.auxio.musikr.model.graph
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.oxycblt.auxio.musikr.model.impl.AlbumImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.SongImpl
|
||||
|
||||
class AlbumLinker {
|
||||
private val tree = mutableMapOf<String?, MutableMap<UUID?, AlbumLink>>()
|
||||
|
||||
fun register(linkedSongs: Flow<ArtistLinker.LinkedSong>) =
|
||||
linkedSongs.map {
|
||||
val nameKey = it.linkedAlbum.preAlbum.rawName.lowercase()
|
||||
val musicBrainzIdKey = it.linkedAlbum.preAlbum.musicBrainzId
|
||||
val albumLink =
|
||||
tree
|
||||
.getOrPut(nameKey) { mutableMapOf() }
|
||||
.getOrPut(musicBrainzIdKey) { AlbumLink(AlbumNode(Contribution())) }
|
||||
albumLink.node.contributors.contribute(it.linkedAlbum)
|
||||
LinkedSong(it, albumLink)
|
||||
}
|
||||
|
||||
fun resolve(): Collection<AlbumImpl> =
|
||||
tree.values.flatMap { musicBrainzIdBundle ->
|
||||
val only = musicBrainzIdBundle.values.singleOrNull()
|
||||
if (only != null) {
|
||||
return@flatMap listOf(only.node.resolve())
|
||||
}
|
||||
val nullBundle =
|
||||
musicBrainzIdBundle[null]
|
||||
?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() }
|
||||
// Only partially tagged with MBIDs, must go through and
|
||||
musicBrainzIdBundle
|
||||
.filter { it.key != null }
|
||||
.forEach {
|
||||
val candidates = it.value.node.contributors.candidates
|
||||
nullBundle.node.contributors.contribute(candidates)
|
||||
it.value.node = nullBundle.node
|
||||
}
|
||||
listOf(nullBundle.node.resolve())
|
||||
}
|
||||
|
||||
data class LinkedSong(
|
||||
val linkedArtistSong: ArtistLinker.LinkedSong,
|
||||
val album: Linked<AlbumImpl, SongImpl>
|
||||
)
|
||||
|
||||
private data class AlbumLink(var node: AlbumNode) : Linked<AlbumImpl, SongImpl> {
|
||||
override fun resolve(child: SongImpl): AlbumImpl {
|
||||
return requireNotNull(node.albumImpl) { "Album not resolved yet" }
|
||||
.also { it.link(child) }
|
||||
}
|
||||
}
|
||||
|
||||
private class AlbumNode(val contributors: Contribution<ArtistLinker.LinkedAlbum>) {
|
||||
var albumImpl: AlbumImpl? = null
|
||||
private set
|
||||
|
||||
fun resolve(): AlbumImpl {
|
||||
val impl = AlbumImpl(LinkedAlbumImpl(contributors.resolve()))
|
||||
albumImpl = impl
|
||||
return impl
|
||||
}
|
||||
}
|
||||
|
||||
private class LinkedAlbumImpl(private val artistLinkedAlbum: ArtistLinker.LinkedAlbum) :
|
||||
LinkedAlbum {
|
||||
override val preAlbum = artistLinkedAlbum.preAlbum
|
||||
|
||||
override val artists = artistLinkedAlbum.artists
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* ArtistLinker.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.auxio.musikr.model.graph
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.musikr.model.impl.AlbumImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.ArtistImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.SongImpl
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreAlbum
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreArtist
|
||||
|
||||
class ArtistLinker {
|
||||
private val tree = mutableMapOf<String?, MutableMap<UUID?, ArtistLink>>()
|
||||
|
||||
fun register(linkedSongs: Flow<GenreLinker.LinkedSong>) =
|
||||
linkedSongs.map {
|
||||
val linkedSongArtists =
|
||||
it.preSong.preArtists.map { artist ->
|
||||
val nameKey = artist.rawName?.lowercase()
|
||||
val musicBrainzIdKey = artist.musicBrainzId
|
||||
val artistLink =
|
||||
tree
|
||||
.getOrPut(nameKey) { mutableMapOf() }
|
||||
.getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) }
|
||||
artistLink.node.contributors.contribute(artist)
|
||||
artistLink
|
||||
}
|
||||
val linkedAlbumArtists =
|
||||
it.preSong.preAlbum.preArtists.map { artist ->
|
||||
val nameKey = artist.rawName?.lowercase()
|
||||
val musicBrainzIdKey = artist.musicBrainzId
|
||||
val artistLink =
|
||||
tree
|
||||
.getOrPut(nameKey) { mutableMapOf() }
|
||||
.getOrPut(musicBrainzIdKey) { ArtistLink(ArtistNode(Contribution())) }
|
||||
artistLink.node.contributors.contribute(artist)
|
||||
artistLink
|
||||
}
|
||||
val linkedAlbum = LinkedAlbum(it.preSong.preAlbum, MultiArtistLink(linkedAlbumArtists))
|
||||
LinkedSong(it, linkedAlbum, MultiArtistLink(linkedSongArtists))
|
||||
}
|
||||
|
||||
fun resolve(): Collection<ArtistImpl> =
|
||||
tree.values.flatMap { musicBrainzIdBundle ->
|
||||
val only = musicBrainzIdBundle.values.singleOrNull()
|
||||
if (only != null) {
|
||||
return@flatMap listOf(only.node.resolve())
|
||||
}
|
||||
val nullBundle =
|
||||
musicBrainzIdBundle[null]
|
||||
?: return@flatMap musicBrainzIdBundle.values.map { it.node.resolve() }
|
||||
// Only partially tagged with MBIDs, must go through and
|
||||
musicBrainzIdBundle
|
||||
.filter { it.key != null }
|
||||
.forEach {
|
||||
val candidates = it.value.node.contributors.candidates
|
||||
nullBundle.node.contributors.contribute(candidates)
|
||||
it.value.node = nullBundle.node
|
||||
}
|
||||
listOf(nullBundle.node.resolve())
|
||||
}
|
||||
|
||||
data class LinkedSong(
|
||||
val linkedGenreSong: GenreLinker.LinkedSong,
|
||||
val linkedAlbum: LinkedAlbum,
|
||||
val artists: Linked<List<ArtistImpl>, SongImpl>
|
||||
)
|
||||
|
||||
data class LinkedAlbum(
|
||||
val preAlbum: PreAlbum,
|
||||
val artists: Linked<List<ArtistImpl>, AlbumImpl>
|
||||
)
|
||||
|
||||
private class MultiArtistLink<T : Music>(val links: List<Linked<ArtistImpl, Music>>) :
|
||||
Linked<List<ArtistImpl>, T> {
|
||||
override fun resolve(child: T): List<ArtistImpl> {
|
||||
return links.map { it.resolve(child) }.distinct()
|
||||
}
|
||||
}
|
||||
|
||||
private data class ArtistLink(var node: ArtistNode) : Linked<ArtistImpl, Music> {
|
||||
override fun resolve(child: Music): ArtistImpl {
|
||||
return requireNotNull(node.artistImpl) { "Artist not resolved yet" }
|
||||
.also {
|
||||
when (child) {
|
||||
is SongImpl -> it.link(child)
|
||||
is AlbumImpl -> it.link(child)
|
||||
else -> error("Cannot link to child $child")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ArtistNode(val contributors: Contribution<PreArtist>) {
|
||||
var artistImpl: ArtistImpl? = null
|
||||
private set
|
||||
|
||||
fun resolve(): ArtistImpl {
|
||||
val impl = ArtistImpl(contributors.resolve())
|
||||
artistImpl = impl
|
||||
return impl
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* GenreLinker.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.auxio.musikr.model.graph
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.oxycblt.auxio.musikr.model.impl.GenreImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.SongImpl
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreGenre
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreSong
|
||||
|
||||
class GenreLinker {
|
||||
private val tree = mutableMapOf<String?, GenreLink>()
|
||||
|
||||
fun register(preSong: Flow<PreSong>): Flow<LinkedSong> =
|
||||
preSong.map {
|
||||
val genreLinks =
|
||||
it.preGenres.map { genre ->
|
||||
val nameKey = genre.rawName?.lowercase()
|
||||
val link = tree.getOrPut(nameKey) { GenreLink(GenreNode(Contribution())) }
|
||||
link.node.contributors.contribute(genre)
|
||||
link
|
||||
}
|
||||
LinkedSong(it, MultiGenreLink(genreLinks))
|
||||
}
|
||||
|
||||
fun resolve() = tree.values.map { it.node.resolve() }
|
||||
|
||||
data class LinkedSong(val preSong: PreSong, val genres: Linked<List<GenreImpl>, SongImpl>)
|
||||
|
||||
private class MultiGenreLink(val links: List<Linked<GenreImpl, SongImpl>>) :
|
||||
Linked<List<GenreImpl>, SongImpl> {
|
||||
override fun resolve(child: SongImpl): List<GenreImpl> {
|
||||
return links.map { it.resolve(child) }.distinct()
|
||||
}
|
||||
}
|
||||
|
||||
private data class GenreLink(var node: GenreNode) : Linked<GenreImpl, SongImpl> {
|
||||
override fun resolve(child: SongImpl): GenreImpl {
|
||||
return requireNotNull(node.genreImpl) { "Genre not resolved yet" }
|
||||
.also { it.link(child) }
|
||||
}
|
||||
}
|
||||
|
||||
private class GenreNode(val contributors: Contribution<PreGenre>) {
|
||||
var genreImpl: GenreImpl? = null
|
||||
private set
|
||||
|
||||
fun resolve(): GenreImpl {
|
||||
val impl = GenreImpl(contributors.resolve())
|
||||
genreImpl = impl
|
||||
return impl
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* LinkedMusic.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.auxio.musikr.model.graph
|
||||
|
||||
import org.oxycblt.auxio.musikr.model.impl.AlbumImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.ArtistImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.GenreImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.PlaylistImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.SongImpl
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreAlbum
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PrePlaylist
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreSong
|
||||
|
||||
interface LinkedSong {
|
||||
val preSong: PreSong
|
||||
val album: Linked<AlbumImpl, SongImpl>
|
||||
val artists: Linked<List<ArtistImpl>, SongImpl>
|
||||
val genres: Linked<List<GenreImpl>, SongImpl>
|
||||
}
|
||||
|
||||
interface LinkedAlbum {
|
||||
val preAlbum: PreAlbum
|
||||
val artists: Linked<List<ArtistImpl>, AlbumImpl>
|
||||
}
|
||||
|
||||
interface LinkedPlaylist {
|
||||
val prePlaylist: PrePlaylist
|
||||
val songs: Linked<List<SongImpl>, PlaylistImpl>
|
||||
}
|
||||
|
||||
interface Linked<P, C> {
|
||||
fun resolve(child: C): P
|
||||
}
|
|
@ -22,28 +22,16 @@ import javax.inject.Inject
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.musikr.model.graph.AlbumLinker
|
||||
import org.oxycblt.auxio.musikr.model.graph.ArtistLinker
|
||||
import org.oxycblt.auxio.musikr.model.graph.GenreLinker
|
||||
import org.oxycblt.auxio.musikr.model.graph.Linked
|
||||
import org.oxycblt.auxio.musikr.model.graph.LinkedSong
|
||||
import org.oxycblt.auxio.musikr.model.impl.AlbumImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.ArtistImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.GenreImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.LibraryImpl
|
||||
import org.oxycblt.auxio.musikr.model.impl.MutableLibrary
|
||||
import org.oxycblt.auxio.musikr.model.impl.SongImpl
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.musikr.graph.MusicGraph
|
||||
import org.oxycblt.auxio.musikr.model.LibraryFactory
|
||||
import org.oxycblt.auxio.musikr.model.MutableLibrary
|
||||
import org.oxycblt.auxio.musikr.tag.Interpretation
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.PreSong
|
||||
import org.oxycblt.auxio.musikr.tag.interpret.TagInterpreter
|
||||
import timber.log.Timber
|
||||
|
||||
interface EvaluateStep {
|
||||
suspend fun evaluate(
|
||||
|
@ -56,6 +44,8 @@ class EvaluateStepImpl
|
|||
@Inject
|
||||
constructor(
|
||||
private val tagInterpreter: TagInterpreter,
|
||||
private val musicGraphFactory: MusicGraph.Factory,
|
||||
private val libraryFactory: LibraryFactory
|
||||
) : EvaluateStep {
|
||||
override suspend fun evaluate(
|
||||
interpretation: Interpretation,
|
||||
|
@ -67,60 +57,9 @@ constructor(
|
|||
.map { tagInterpreter.interpret(it.file, it.tags, interpretation) }
|
||||
.flowOn(Dispatchers.Main)
|
||||
.buffer(Channel.UNLIMITED)
|
||||
|
||||
val genreLinker = GenreLinker()
|
||||
val genreLinkedSongs =
|
||||
genreLinker.register(preSongs).flowOn(Dispatchers.Main).buffer(Channel.UNLIMITED)
|
||||
|
||||
val artistLinker = ArtistLinker()
|
||||
val artistLinkedSongs =
|
||||
artistLinker.register(genreLinkedSongs).flowOn(Dispatchers.Main).toList()
|
||||
// This is intentional. Song and album instances are dependent on artist
|
||||
// data, so we need to ensure that all of the linked artist data is resolved
|
||||
// before we go any further.
|
||||
val genres = genreLinker.resolve()
|
||||
val artists = artistLinker.resolve()
|
||||
|
||||
val albumLinker = AlbumLinker()
|
||||
val albumLinkedSongs =
|
||||
albumLinker
|
||||
.register(artistLinkedSongs.asFlow())
|
||||
.map { LinkedSongImpl(it) }
|
||||
.flowOn(Dispatchers.Main)
|
||||
.toList()
|
||||
val albums = albumLinker.resolve()
|
||||
|
||||
val uidMap = mutableMapOf<Music.UID, SongImpl>()
|
||||
val songs =
|
||||
albumLinkedSongs.mapNotNull {
|
||||
val uid = it.preSong.computeUid()
|
||||
val other = uidMap[uid]
|
||||
if (other == null) {
|
||||
SongImpl(it)
|
||||
} else {
|
||||
Timber.d("Song @ $uid already exists at ${other.path}, ignoring")
|
||||
null
|
||||
}
|
||||
}
|
||||
return LibraryImpl(
|
||||
songs,
|
||||
albums.onEach { it.finalize() },
|
||||
artists.onEach { it.finalize() },
|
||||
genres.onEach { it.finalize() })
|
||||
}
|
||||
|
||||
private data class LinkedSongImpl(private val albumLinkedSong: AlbumLinker.LinkedSong) :
|
||||
LinkedSong {
|
||||
override val preSong: PreSong
|
||||
get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.preSong
|
||||
|
||||
override val album: Linked<AlbumImpl, SongImpl>
|
||||
get() = albumLinkedSong.album
|
||||
|
||||
override val artists: Linked<List<ArtistImpl>, SongImpl>
|
||||
get() = albumLinkedSong.linkedArtistSong.artists
|
||||
|
||||
override val genres: Linked<List<GenreImpl>, SongImpl>
|
||||
get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.genres
|
||||
val graphBuilder = musicGraphFactory.builder()
|
||||
preSongs.collect { graphBuilder.add(it) }
|
||||
val graph = graphBuilder.build()
|
||||
return libraryFactory.create(graph)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue