diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index bcf29ca6e..4b53254e8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/PlaylistLinker.kt b/app/src/main/java/org/oxycblt/auxio/musikr/graph/GraphModule.kt similarity index 55% rename from app/src/main/java/org/oxycblt/auxio/musikr/model/graph/PlaylistLinker.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/graph/GraphModule.kt index 44f33bec6..49fa786ae 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/PlaylistLinker.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/graph/GraphModule.kt @@ -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 . */ -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, - linkedSongs: Flow - ): Flow = emptyFlow() - - fun resolve(): Collection = setOf() +@Module +@InstallIn(SingletonComponent::class) +interface GraphModule { + @Binds fun musicGraphFactory(interpreter: MusicGraphFactoryImpl): MusicGraph.Factory } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/graph/MusicGraph.kt b/app/src/main/java/org/oxycblt/auxio/musikr/graph/MusicGraph.kt new file mode 100644 index 000000000..fd067e4b1 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/musikr/graph/MusicGraph.kt @@ -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 . + */ + +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, + val albumVertex: List, + val artistVertex: List, + val genreVertex: List +) { + 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() + private val albumVertices = mutableMapOf() + private val artistVertices = mutableMapOf() + private val genreVertices = mutableMapOf() + + 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) { + // 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) { + val fullMusicBrainzIdCoverage = cluster.all { it.preArtist.musicBrainzId != null } + if (fullMusicBrainzIdCoverage) { + // All artists have MBIDs, nothing needs to be merged. + val mbidClusters = cluster.groupBy { unlikelyToBeNull(it.preArtist.musicBrainzId) } + for (mbidCluster in mbidClusters.values) { + simplifyArtistClusterImpl(mbidCluster) + } + return + } + // No full MBID coverage, discard the MBIDs from the graph. + val strippedCluster = + cluster.map { + val noMbidPreArtist = it.preArtist.copy(musicBrainzId = null) + val simpleMbidVertex = + artistVertices.getOrPut(noMbidPreArtist) { ArtistVertex(noMbidPreArtist) } + meldArtistVertices(it, simpleMbidVertex) + simpleMbidVertex + } + simplifyArtistClusterImpl(strippedCluster) + } + + private fun simplifyArtistClusterImpl(cluster: Collection) { + if (cluster.size == 1) { + // One canonical artist, nothing to collapse + return + } + val clusterSet = cluster.toMutableSet() + val relevantArtistVertex = clusterSet.maxBy { it.songVertices.size } + clusterSet.remove(relevantArtistVertex) + for (irrelevantArtistVertex in clusterSet) { + meldArtistVertices(irrelevantArtistVertex, relevantArtistVertex) + } + } + + private fun meldArtistVertices(src: ArtistVertex, dst: ArtistVertex) { + // 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) { + val fullMusicBrainzIdCoverage = cluster.all { it.preAlbum.musicBrainzId != null } + if (fullMusicBrainzIdCoverage) { + // All albums have MBIDs, nothing needs to be merged. + val mbidClusters = cluster.groupBy { unlikelyToBeNull(it.preAlbum.musicBrainzId) } + for (mbidCluster in mbidClusters.values) { + simplifyAlbumClusterImpl(mbidCluster) + } + return + } + // No full MBID coverage, discard the MBIDs from the graph. + val strippedCluster = + cluster.map { + val noMbidPreAlbum = it.preAlbum.copy(musicBrainzId = null) + val simpleMbidVertex = + albumVertices.getOrPut(noMbidPreAlbum) { + AlbumVertex(noMbidPreAlbum, it.artistVertices.toMutableList()) + } + meldAlbumVertices(it, simpleMbidVertex) + simpleMbidVertex + } + simplifyAlbumClusterImpl(strippedCluster) + } + + private fun simplifyAlbumClusterImpl(cluster: Collection) { + // All of these albums are semantically equivalent. Pick the most popular variation + // and merge all the others into it. + 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, + var genreVertices: MutableList +) { + var tag: Any? = null +} + +class AlbumVertex(val preAlbum: PreAlbum, var artistVertices: MutableList) { + val songVertices = mutableSetOf() + var tag: Any? = null +} + +class ArtistVertex( + val preArtist: PreArtist, +) { + val songVertices = mutableSetOf() + val albumVertices = mutableSetOf() + val genreVertices = mutableSetOf() + var tag: Any? = null +} + +class GenreVertex(val preGenre: PreGenre) { + val songVertices = mutableSetOf() + val artistVertices = mutableSetOf() + var tag: Any? = null +} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/DeviceMusicImpl.kt similarity index 51% rename from app/src/main/java/org/oxycblt/auxio/musikr/model/impl/DeviceMusicImpl.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/model/DeviceMusicImpl.kt index 53108f23b..147988b67 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/model/DeviceMusicImpl.kt @@ -16,10 +16,8 @@ * along with this program. If not, see . */ -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 + + fun resolveGenres(): List +} + /** * 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 + get() = handle.resolveArtists() + + override val genres: List + 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 + + fun resolveArtists(): List +} + /** * 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() + override val artists: List + 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 + val albums: Set - fun finalize() { - cover = Cover.single(Sort(Sort.Mode.ByTrack, Sort.Direction.ASCENDING).songs(songs).first()) - } + fun resolveGenres(): Set } /** @@ -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() + 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() - override var explicitAlbums = mutableSetOf() - override var implicitAlbums = mutableSetOf() + override val genres: List + get() = handle.resolveGenres().toList() - override var genres = listOf() - 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 + val artists: Set } /** @@ -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() override val artists = mutableSetOf() - 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 }) - } } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/Library.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/Library.kt similarity index 51% rename from app/src/main/java/org/oxycblt/auxio/musikr/model/impl/Library.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/model/Library.kt index 37ca090f2..40296a494 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/Library.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/model/Library.kt @@ -16,13 +16,22 @@ * along with this program. If not, see . */ -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): 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, override val albums: Collection, diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/Contribution.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/ModelModule.kt similarity index 59% rename from app/src/main/java/org/oxycblt/auxio/musikr/model/graph/Contribution.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/model/ModelModule.kt index b7f3bf0b3..9918a11b2 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/Contribution.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/model/ModelModule.kt @@ -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 . */ -package org.oxycblt.auxio.musikr.model.graph +package org.oxycblt.auxio.musikr.model -class Contribution { - private val map = mutableMapOf() +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent - val candidates: Collection - get() = map.keys - - fun contribute(key: T) { - map[key] = map.getOrDefault(key, 0) + 1 - } - - fun contribute(keys: Collection) { - 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 } diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/PlaylistImpl.kt similarity index 54% rename from app/src/main/java/org/oxycblt/auxio/musikr/model/impl/PlaylistImpl.kt rename to app/src/main/java/org/oxycblt/auxio/musikr/model/PlaylistImpl.kt index 664fd9a63..a350a2cce 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/impl/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/model/PlaylistImpl.kt @@ -16,29 +16,33 @@ * along with this program. If not, see . */ -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 +} - 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 diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/AlbumLinker.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/AlbumLinker.kt deleted file mode 100644 index 64b5d87aa..000000000 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/AlbumLinker.kt +++ /dev/null @@ -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 . - */ - -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>() - - fun register(linkedSongs: Flow) = - 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 = - 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 - ) - - private data class AlbumLink(var node: AlbumNode) : Linked { - override fun resolve(child: SongImpl): AlbumImpl { - return requireNotNull(node.albumImpl) { "Album not resolved yet" } - .also { it.link(child) } - } - } - - private class AlbumNode(val contributors: Contribution) { - 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 - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/ArtistLinker.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/ArtistLinker.kt deleted file mode 100644 index 3f8972bec..000000000 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/ArtistLinker.kt +++ /dev/null @@ -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 . - */ - -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>() - - fun register(linkedSongs: Flow) = - 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 = - 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, SongImpl> - ) - - data class LinkedAlbum( - val preAlbum: PreAlbum, - val artists: Linked, AlbumImpl> - ) - - private class MultiArtistLink(val links: List>) : - Linked, T> { - override fun resolve(child: T): List { - return links.map { it.resolve(child) }.distinct() - } - } - - private data class ArtistLink(var node: ArtistNode) : Linked { - 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) { - var artistImpl: ArtistImpl? = null - private set - - fun resolve(): ArtistImpl { - val impl = ArtistImpl(contributors.resolve()) - artistImpl = impl - return impl - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/GenreLinker.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/GenreLinker.kt deleted file mode 100644 index 8e58ae916..000000000 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/GenreLinker.kt +++ /dev/null @@ -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 . - */ - -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() - - fun register(preSong: Flow): Flow = - 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, SongImpl>) - - private class MultiGenreLink(val links: List>) : - Linked, SongImpl> { - override fun resolve(child: SongImpl): List { - return links.map { it.resolve(child) }.distinct() - } - } - - private data class GenreLink(var node: GenreNode) : Linked { - override fun resolve(child: SongImpl): GenreImpl { - return requireNotNull(node.genreImpl) { "Genre not resolved yet" } - .also { it.link(child) } - } - } - - private class GenreNode(val contributors: Contribution) { - var genreImpl: GenreImpl? = null - private set - - fun resolve(): GenreImpl { - val impl = GenreImpl(contributors.resolve()) - genreImpl = impl - return impl - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/LinkedMusic.kt b/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/LinkedMusic.kt deleted file mode 100644 index 4bbc360c1..000000000 --- a/app/src/main/java/org/oxycblt/auxio/musikr/model/graph/LinkedMusic.kt +++ /dev/null @@ -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 . - */ - -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 - val artists: Linked, SongImpl> - val genres: Linked, SongImpl> -} - -interface LinkedAlbum { - val preAlbum: PreAlbum - val artists: Linked, AlbumImpl> -} - -interface LinkedPlaylist { - val prePlaylist: PrePlaylist - val songs: Linked, PlaylistImpl> -} - -interface Linked { - fun resolve(child: C): P -} diff --git a/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/EvaluateStep.kt b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/EvaluateStep.kt index 6d4c0d5dd..64581299e 100644 --- a/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/EvaluateStep.kt +++ b/app/src/main/java/org/oxycblt/auxio/musikr/pipeline/EvaluateStep.kt @@ -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() - 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 - get() = albumLinkedSong.album - - override val artists: Linked, SongImpl> - get() = albumLinkedSong.linkedArtistSong.artists - - override val genres: Linked, SongImpl> - get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.genres + val graphBuilder = musicGraphFactory.builder() + preSongs.collect { graphBuilder.add(it) } + val graph = graphBuilder.build() + return libraryFactory.create(graph) } }