diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/ArtistTree.kt b/app/src/main/java/org/oxycblt/auxio/music/device/ArtistTree.kt new file mode 100644 index 000000000..0519bd15b --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/device/ArtistTree.kt @@ -0,0 +1,42 @@ +package org.oxycblt.auxio.music.device + + +interface AlbumTree { + fun register(linkedSong: ArtistTree.LinkedSong): LinkedSong + fun resolve(): Collection + + data class LinkedSong( + val linkedArtistSong: ArtistTree.LinkedSong, + val album: Linked + ) +} + +interface ArtistTree { + fun register(preSong: GenreTree.LinkedSong): LinkedSong + fun resolve(): Collection + + data class LinkedSong( + val linkedGenreSong: GenreTree.LinkedSong, + val linkedAlbum: LinkedAlbum, + val artists: Linked, SongImpl> + ) + + data class LinkedAlbum( + val preAlbum: PreAlbum, + val artists: Linked, AlbumImpl> + ) +} + +interface GenreTree { + fun register(preSong: PreSong): LinkedSong + fun resolve(): Collection + + data class LinkedSong( + val preSong: PreSong, + val genres: Linked, SongImpl> + ) +} + +interface Linked { + fun resolve(child: C): P +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt index c10f4d03f..72eb8e63c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt @@ -127,7 +127,25 @@ interface DeviceLibrary { processedSongs: Channel, separators: Separators, nameFactory: Name.Known.Factory - ): DeviceLibraryImpl + ): DeviceLibrary + } +} + +class DeviceLibraryFactoryImpl2 @Inject constructor( + val interpreterFactory: Interpreter.Factory +) : DeviceLibrary.Factory { + override suspend fun create( + rawSongs: Channel, + processedSongs: Channel, + separators: Separators, + nameFactory: Name.Known.Factory + ): DeviceLibrary { + val interpreter = interpreterFactory.create(nameFactory, separators) + rawSongs.forEachWithTimeout { rawSong -> + interpreter.consume(rawSong) + processedSongs.sendWithTimeout(rawSong) + } + return interpreter.resolve() } } @@ -137,7 +155,7 @@ class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory { processedSongs: Channel, separators: Separators, nameFactory: Name.Known.Factory - ): DeviceLibraryImpl { + ): DeviceLibrary { val songGrouping = mutableMapOf() val albumGrouping = mutableMapOf>>() val artistGrouping = mutableMapOf>>() @@ -185,9 +203,7 @@ class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory { // Now that all songs are processed, also process albums and group them into their // respective artists. - pruneMusicBrainzIdTree(albumGrouping) { old, new -> - compareSongTracks(old, new) - } + pruneMusicBrainzIdTree(albumGrouping) { old, new -> compareSongTracks(old, new) } val albums = flattenMusicBrainzIdTree(albumGrouping) { AlbumImpl(it, nameFactory) } for (album in albums) { for (rawArtist in album.rawArtists) { @@ -214,7 +230,7 @@ class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory { compareSongDates(old, new) } old is AlbumImpl && new is AlbumImpl -> { - compareAlbumDates(old, new) + compareAlbumDates(old, new) } else -> throw IllegalStateException() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt index 85e8e511e..773e97282 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt @@ -26,5 +26,6 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) interface DeviceModule { - @Binds fun deviceLibraryFactory(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory + @Binds fun deviceLibraryFactory(factory: DeviceLibraryFactoryImpl2): DeviceLibrary.Factory + @Binds fun interpreterFactory(factory: InterpreterFactoryImpl): Interpreter.Factory } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index 1a90b4cd7..cdf34fcc6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -15,11 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.music.device import org.oxycblt.auxio.R -import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.image.extractor.ParentCover import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Album @@ -28,346 +27,95 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.fs.MimeType -import org.oxycblt.auxio.music.fs.toAlbumCoverUri -import org.oxycblt.auxio.music.fs.toAudioUri -import org.oxycblt.auxio.music.fs.toSongCoverUri import org.oxycblt.auxio.music.info.Date -import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Name -import org.oxycblt.auxio.music.info.ReleaseType -import org.oxycblt.auxio.music.metadata.Separators -import org.oxycblt.auxio.music.metadata.parseId3GenreNames -import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment import org.oxycblt.auxio.util.positiveOrNull -import org.oxycblt.auxio.util.toUuidOrNull -import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.update +import kotlin.math.min /** * Library-backed implementation of [Song]. * - * @param rawSong The [RawSong] to derive the member data from. - * @param nameFactory The [Name.Known.Factory] to interpret name information with. - * @param separators The [Separators] to parse multi-value tags with. + * @param linkedSong The completed [LinkedSong] all metadata van be inferred from * @author Alexander Capehart (OxygenCobalt) */ -class SongImpl( - private val rawSong: RawSong, - private val nameFactory: Name.Known.Factory, - private val separators: Separators -) : Song { +class SongImpl(linkedSong: LinkedSong) : Song { + private val preSong = linkedSong.preSong + override val uid = // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. - rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicType.SONGS, it) } + preSong.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.SONGS, it) } ?: Music.UID.auxio(MusicType.SONGS) { // Song UIDs are based on the raw data without parsing so that they remain // consistent across music setting changes. Parents are not held up to the // same standard since grouping is already inherently linked to settings. - update(rawSong.name) - update(rawSong.albumName) - update(rawSong.date) + update(preSong.rawName) + update(preSong.preAlbum.rawName) + update(preSong.date) - update(rawSong.track) - update(rawSong.disc) + update(preSong.track) + update(preSong.disc?.number) - update(rawSong.artistNames) - update(rawSong.albumArtistNames) + update(preSong.preArtists.map { it.rawName }) + update(preSong.preAlbum.preArtists.map { it.rawName }) } - override val name = - nameFactory.parse( - requireNotNull(rawSong.name) { "Invalid raw ${rawSong.path}: No title" }, - rawSong.sortName) + override val name = preSong.name + override val track = preSong.track + override val disc = preSong.disc + override val date = preSong.date + override val uri = preSong.uri + override val cover = preSong.cover + override val path = preSong.path + override val mimeType = preSong.mimeType + override val size = preSong.size + override val durationMs = preSong.durationMs + override val replayGainAdjustment = preSong.replayGainAdjustment + override val dateAdded = preSong.dateAdded + override val album = linkedSong.album.resolve(this) + override val artists = linkedSong.artists.resolve(this) + override val genres = linkedSong.genres.resolve(this) - override val track = rawSong.track - override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) } - override val date = rawSong.date - override val uri = - requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }.toAudioUri() - override val path = requireNotNull(rawSong.path) { "Invalid raw ${rawSong.path}: No path" } - override val mimeType = - MimeType( - fromExtension = - requireNotNull(rawSong.extensionMimeType) { - "Invalid raw ${rawSong.path}: No mime type" - }, - fromFormat = null) - override val size = requireNotNull(rawSong.size) { "Invalid raw ${rawSong.path}: No size" } - override val durationMs = - requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.path}: No duration" } - override val replayGainAdjustment = - ReplayGainAdjustment( - track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment) - - override val dateAdded = - requireNotNull(rawSong.dateAdded) { "Invalid raw ${rawSong.path}: No date added" } - - private var _album: AlbumImpl? = null - override val album: Album - get() = unlikelyToBeNull(_album) - - private val _artists = mutableListOf() - override val artists: List - get() = _artists - - private val _genres = mutableListOf() - override val genres: List - get() = _genres - - override val cover = - rawSong.coverPerceptualHash?.let { - // We were able to confirm that the song had a parsable cover and can be used on - // a per-song basis. Otherwise, just fall back to a per-album cover instead, as - // it implies either a cover.jpg pattern is used (likely) or ExoPlayer does not - // support the cover metadata of a given spec (unlikely). - Cover.Embedded( - requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" } - .toSongCoverUri(), - uri, - it) - } ?: Cover.External(requireNotNull(rawSong.albumMediaStoreId).toAlbumCoverUri()) - - /** - * The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an - * [Album]. - */ - val rawAlbum: RawAlbum - - /** - * The [RawArtist] instances collated by the [Song]. The artists of the song take priority, - * followed by the album artists. If there are no artists, this field will be a single "unknown" - * [RawArtist]. This can be used to group up [Song]s into an [Artist]. - */ - val rawArtists: List - - /** - * The [RawGenre] instances collated by the [Song]. This can be used to group up [Song]s into a - * [Genre]. ID3v2 Genre names are automatically converted to their resolved names. - */ - val rawGenres: List - - private var hashCode: Int = uid.hashCode() - - init { - val artistMusicBrainzIds = separators.split(rawSong.artistMusicBrainzIds) - val artistNames = separators.split(rawSong.artistNames) - val artistSortNames = separators.split(rawSong.artistSortNames) - val rawIndividualArtists = - artistNames - .mapIndexed { i, name -> - RawArtist( - artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), - name, - artistSortNames.getOrNull(i)) - } - // Some songs have the same artist listed multiple times (sometimes with different - // casing!), - // so we need to deduplicate lest finalization reordering fails. - // Since MBID data can wind up clobbered later in the grouper, we can't really - // use it to deduplicate. That means that a hypothetical track with two artists - // of the same name but different MBIDs will be grouped wrong. That is a bridge - // I will cross when I get to it. - .distinctBy { it.name?.lowercase() } - - val albumArtistMusicBrainzIds = separators.split(rawSong.albumArtistMusicBrainzIds) - val albumArtistNames = separators.split(rawSong.albumArtistNames) - val albumArtistSortNames = separators.split(rawSong.albumArtistSortNames) - val rawAlbumArtists = - albumArtistNames - .mapIndexed { i, name -> - RawArtist( - albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(), - name, - albumArtistSortNames.getOrNull(i)) - } - .distinctBy { it.name?.lowercase() } - - rawAlbum = - RawAlbum( - mediaStoreId = - requireNotNull(rawSong.albumMediaStoreId) { - "Invalid raw ${rawSong.path}: No album id" - }, - musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(), - name = - requireNotNull(rawSong.albumName) { - "Invalid raw ${rawSong.path}: No album name" - }, - sortName = rawSong.albumSortName, - releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)), - rawArtists = - rawAlbumArtists - .ifEmpty { rawIndividualArtists } - .ifEmpty { listOf(RawArtist()) }) - - rawArtists = - rawIndividualArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(RawArtist()) } - - val genreNames = - (rawSong.genreNames.parseId3GenreNames() ?: separators.split(rawSong.genreNames)) - rawGenres = - genreNames - .map { RawGenre(it) } - .distinctBy { it.name?.lowercase() } - .ifEmpty { listOf(RawGenre()) } - - hashCode = 31 * hashCode + rawSong.hashCode() - hashCode = 31 * hashCode + nameFactory.hashCode() - } + private val hashCode = 31 * uid.hashCode() + preSong.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 SongImpl && - uid == other.uid && - nameFactory == other.nameFactory && - separators == other.separators && - rawSong == other.rawSong + uid == other.uid && + preSong == other.preSong override fun toString() = "Song(uid=$uid, name=$name)" - - /** - * Links this [Song] with a parent [Album]. - * - * @param album The parent [Album] to link to. - */ - fun link(album: AlbumImpl) { - _album = album - } - - /** - * Links this [Song] with a parent [Artist]. - * - * @param artist The parent [Artist] to link to. - */ - fun link(artist: ArtistImpl) { - _artists.add(artist) - } - - /** - * Links this [Song] with a parent [Genre]. - * - * @param genre The parent [Genre] to link to. - */ - fun link(genre: GenreImpl) { - _genres.add(genre) - } - - /** - * Perform final validation and organization on this instance. - * - * @return This instance upcasted to [Song]. - */ - fun finalize(): Song { - checkNotNull(_album) { "Malformed song ${path}: No album" } - - check(_artists.isNotEmpty()) { "Malformed song ${path}: No artists" } - check(_artists.size == rawArtists.size) { - "Malformed song ${path}: Artist grouping mismatch" - } - for (i in _artists.indices) { - // Non-destructively reorder the linked artists so that they align with - // the artist ordering within the song metadata. - val newIdx = _artists[i].getOriginalPositionIn(rawArtists) - val other = _artists[newIdx] - _artists[newIdx] = _artists[i] - _artists[i] = other - } - - check(_genres.isNotEmpty()) { "Malformed song ${path}: No genres" } - check(_genres.size == rawGenres.size) { "Malformed song ${path}: Genre grouping mismatch" } - for (i in _genres.indices) { - // Non-destructively reorder the linked genres so that they align with - // the genre ordering within the song metadata. - val newIdx = _genres[i].getOriginalPositionIn(rawGenres) - val other = _genres[newIdx] - _genres[newIdx] = _genres[i] - _genres[i] = other - } - return this - } } /** * Library-backed implementation of [Album]. * - * @param grouping [Grouping] to derive the member data from. - * @param nameFactory The [Name.Known.Factory] to interpret name information with. * @author Alexander Capehart (OxygenCobalt) */ -class AlbumImpl( - grouping: Grouping, - private val nameFactory: Name.Known.Factory -) : Album { - private val rawAlbum = grouping.raw.inner +class AlbumImpl(linkedAlbum: LinkedAlbum) : Album { + private val preAlbum = linkedAlbum.preAlbum override val uid = // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. - rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ALBUMS, it) } + preAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ALBUMS, it) } ?: Music.UID.auxio(MusicType.ALBUMS) { // Hash based on only names despite the presence of a date to increase stability. // I don't know if there is any situation where an artist will have two albums with // the exact same name, but if there is, I would love to know. - update(rawAlbum.name) - update(rawAlbum.rawArtists.map { it.name }) + update(preAlbum.rawName) + update(preAlbum.preArtists.map { it.rawName }) } - override val name = nameFactory.parse(rawAlbum.name, rawAlbum.sortName) - override val dates: Date.Range? - override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null) - override val durationMs: Long - override val dateAdded: Long - override val cover: ParentCover + override val name = preAlbum.name + override val releaseType = preAlbum.releaseType + override var durationMs = 0L + override var dateAdded = 0L + override lateinit var cover: ParentCover + override var dates: Date.Range? = null - private val _artists = mutableListOf() - override val artists: List - get() = _artists + override val artists = linkedAlbum.artists.resolve(this) + override val songs = mutableSetOf() - override val songs: Set = grouping.music - - private var hashCode = uid.hashCode() - - init { - var totalDuration: Long = 0 - var minDate: Date? = null - var maxDate: Date? = null - var earliestDateAdded: Long = Long.MAX_VALUE - - // Do linking and value generation in the same loop for efficiency. - for (song in grouping.music) { - song.link(this) - - if (song.date != null) { - val min = minDate - if (min == null || song.date < min) { - minDate = song.date - } - - val max = maxDate - if (max == null || song.date > max) { - maxDate = song.date - } - } - - if (song.dateAdded < earliestDateAdded) { - earliestDateAdded = song.dateAdded - } - totalDuration += song.durationMs - } - - val min = minDate - val max = maxDate - dates = if (min != null && max != null) Date.Range(min, max) else null - durationMs = totalDuration - dateAdded = earliestDateAdded - - cover = ParentCover.from(grouping.raw.src.cover, songs) - - hashCode = 31 * hashCode + rawAlbum.hashCode() - hashCode = 31 * hashCode + nameFactory.hashCode() - hashCode = 31 * hashCode + songs.hashCode() - } + private var hashCode = 31 * uid.hashCode() + preAlbum.hashCode() override fun hashCode() = hashCode @@ -375,27 +123,24 @@ class AlbumImpl( // we just compare raw instances and how they are interpreted. override fun equals(other: Any?) = other is AlbumImpl && - uid == other.uid && - rawAlbum == other.rawAlbum && - nameFactory == other.nameFactory && - songs == other.songs + uid == other.uid && + preAlbum == other.preAlbum && + songs == other.songs override fun toString() = "Album(uid=$uid, name=$name)" - /** - * The [RawArtist] instances collated by the [Album]. The album artists of the song take - * priority, followed by the artists. If there are no artists, this field will be a single - * "unknown" [RawArtist]. This can be used to group up [Album]s into an [Artist]. - */ - val rawArtists = rawAlbum.rawArtists - - /** - * Links this [Album] with a parent [Artist]. - * - * @param artist The parent [Artist] to link to. - */ - fun link(artist: ArtistImpl) { - _artists.add(artist) + fun link(song: SongImpl) { + songs.add(song) + hashCode = 31 * hashCode + song.hashCode() + 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) + } } /** @@ -404,19 +149,6 @@ class AlbumImpl( * @return This instance upcasted to [Album]. */ fun finalize(): Album { - check(songs.isNotEmpty()) { "Malformed album $name: Empty" } - check(_artists.isNotEmpty()) { "Malformed album $name: No artists" } - check(_artists.size == rawArtists.size) { - "Malformed album $name: Artist grouping mismatch" - } - for (i in _artists.indices) { - // Non-destructively reorder the linked artists so that they align with - // the artist ordering within the song metadata. - val newIdx = _artists[i].getOriginalPositionIn(rawArtists) - val other = _artists[newIdx] - _artists[newIdx] = _artists[i] - _artists[i] = other - } return this } } @@ -465,10 +197,12 @@ class ArtistImpl( albumMap[music.album] = false } } + is AlbumImpl -> { music.link(this) albumMap[music] = true } + else -> error("Unexpected input music $music in $name ${music::class.simpleName}") } } @@ -500,24 +234,13 @@ class ArtistImpl( // we just compare raw instances and how they are interpreted. override fun equals(other: Any?) = other is ArtistImpl && - uid == other.uid && - rawArtist == other.rawArtist && - nameFactory == other.nameFactory && - songs == other.songs + uid == other.uid && + rawArtist == other.rawArtist && + nameFactory == other.nameFactory && + songs == other.songs override fun toString() = "Artist(uid=$uid, name=$name)" - /** - * Returns the original position of this [Artist]'s [RawArtist] within the given [RawArtist] - * list. This can be used to create a consistent ordering within child [Artist] lists based on - * the original tag order. - * - * @param rawArtists The [RawArtist] instances to check. It is assumed that this [Artist]'s - * [RawArtist] will be within the list. - * @return The index of the [Artist]'s [RawArtist] within the list. - */ - fun getOriginalPositionIn(rawArtists: List) = - rawArtists.indexOfFirst { it.name?.lowercase() == rawArtist.name?.lowercase() } /** * Perform final validation and organization on this instance. @@ -593,25 +316,13 @@ class GenreImpl( override fun equals(other: Any?) = other is GenreImpl && - uid == other.uid && - rawGenre == other.rawGenre && - nameFactory == other.nameFactory && - songs == other.songs + uid == other.uid && + rawGenre == other.rawGenre && + nameFactory == other.nameFactory && + songs == other.songs override fun toString() = "Genre(uid=$uid, name=$name)" - /** - * Returns the original position of this [Genre]'s [RawGenre] within the given [RawGenre] list. - * This can be used to create a consistent ordering within child [Genre] lists based on the - * original tag order. - * - * @param rawGenres The [RawGenre] instances to check. It is assumed that this [Genre] 's - * [RawGenre] will be within the list. - * @return The index of the [Genre]'s [RawGenre] within the list. - */ - fun getOriginalPositionIn(rawGenres: List) = - rawGenres.indexOfFirst { it.name?.lowercase() == rawGenre.name?.lowercase() } - /** * Perform final validation and organization on this instance. * diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/Interpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/device/Interpreter.kt new file mode 100644 index 000000000..781f3ee17 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/device/Interpreter.kt @@ -0,0 +1,63 @@ +package org.oxycblt.auxio.music.device + +import org.oxycblt.auxio.music.info.Name +import org.oxycblt.auxio.music.metadata.Separators +import javax.inject.Inject + +interface Interpreter { + fun consume(rawSong: RawSong) + fun resolve(): DeviceLibrary + + interface Factory { + fun create(nameFactory: Name.Known.Factory, separators: Separators): Interpreter + } +} + +class LinkedSong(val albumLinkedSong: AlbumTree.LinkedSong) { + val preSong: PreSong get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.preSong + val album: Linked get() = albumLinkedSong.album + val artists: Linked, SongImpl> get() = albumLinkedSong.linkedArtistSong.artists + val genres: Linked, SongImpl> get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.genres +} + +typealias LinkedAlbum = ArtistTree.LinkedAlbum + +class InterpreterFactoryImpl @Inject constructor( + private val songInterpreterFactory: SongInterpreter.Factory, + private val albumTree: AlbumTree, + private val artistTree: ArtistTree, + private val genreTree: GenreTree +) : Interpreter.Factory { + override fun create(nameFactory: Name.Known.Factory, separators: Separators): Interpreter = + InterpreterImpl( + songInterpreterFactory.create(nameFactory, separators), + albumTree, + artistTree, + genreTree + ) +} + +private class InterpreterImpl( + private val songInterpreter: SongInterpreter, + private val albumTree: AlbumTree, + private val artistTree: ArtistTree, + private val genreTree: GenreTree +) : Interpreter { + private val songs = mutableListOf() + + override fun consume(rawSong: RawSong) { + val preSong = songInterpreter.consume(rawSong) + val genreLinkedSong = genreTree.register(preSong) + val artistLinkedSong = artistTree.register(genreLinkedSong) + val albumLinkedSong = albumTree.register(artistLinkedSong) + songs.add(LinkedSong(albumLinkedSong)) + } + + override fun resolve(): DeviceLibrary { + val genres = genreTree.resolve() + val artists = artistTree.resolve() + val albums = albumTree.resolve() + val songs = songs.map { SongImpl(it) } + return DeviceLibraryImpl(songs, albums, artists, genres) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/PreMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/PreMusic.kt new file mode 100644 index 000000000..9fce2b854 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/device/PreMusic.kt @@ -0,0 +1,51 @@ +package org.oxycblt.auxio.music.device + +import android.net.Uri +import org.oxycblt.auxio.image.extractor.Cover +import org.oxycblt.auxio.music.fs.MimeType +import org.oxycblt.auxio.music.fs.Path +import org.oxycblt.auxio.music.info.Date +import org.oxycblt.auxio.music.info.Disc +import org.oxycblt.auxio.music.info.Name +import org.oxycblt.auxio.music.info.ReleaseType +import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment +import java.util.UUID + +data class PreSong( + val musicBrainzId: UUID?, + val name: Name, + val rawName: String?, + val track: Int?, + val disc: Disc?, + val date: Date?, + val uri: Uri, + val cover: Cover, + val path: Path, + val mimeType: MimeType, + val size: Long, + val durationMs: Long, + val replayGainAdjustment: ReplayGainAdjustment, + val dateAdded: Long, + val preAlbum: PreAlbum, + val preArtists: List, + val preGenres: List +) + +data class PreAlbum( + val musicBrainzId: UUID?, + val name: Name, + val rawName: String, + val releaseType: ReleaseType, + val preArtists: List +) + +data class PreArtist( + val musicBrainzId: UUID?, + val name: Name, + val rawName: String?, +) + +data class PreGenre( + val name: Name, + val rawName: String?, +) diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/SongInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/device/SongInterpreter.kt new file mode 100644 index 000000000..c572a4b3b --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/device/SongInterpreter.kt @@ -0,0 +1,156 @@ +package org.oxycblt.auxio.music.device + +import org.oxycblt.auxio.R +import org.oxycblt.auxio.image.extractor.Cover +import org.oxycblt.auxio.music.fs.MimeType +import org.oxycblt.auxio.music.fs.toAlbumCoverUri +import org.oxycblt.auxio.music.fs.toAudioUri +import org.oxycblt.auxio.music.fs.toSongCoverUri +import org.oxycblt.auxio.music.info.Disc +import org.oxycblt.auxio.music.info.Name +import org.oxycblt.auxio.music.info.ReleaseType +import org.oxycblt.auxio.music.metadata.Separators +import org.oxycblt.auxio.music.metadata.parseId3GenreNames +import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment +import org.oxycblt.auxio.util.toUuidOrNull + +interface SongInterpreter { + fun consume(rawSong: RawSong): PreSong + + interface Factory { + fun create(nameFactory: Name.Known.Factory, separators: Separators): SongInterpreter + } +} + +class SongInterpreterFactory : SongInterpreter.Factory { + override fun create(nameFactory: Name.Known.Factory, separators: Separators) = + SongInterpreterImpl(nameFactory, separators) +} + +class SongInterpreterImpl( + private val nameFactory: Name.Known.Factory, + private val separators: Separators +) : SongInterpreter { + override fun consume(rawSong: RawSong): PreSong { + val individualPreArtists = makePreArtists( + rawSong.artistMusicBrainzIds, + rawSong.artistNames, + rawSong.artistSortNames + ) + val albumPreArtists = makePreArtists( + rawSong.albumArtistMusicBrainzIds, + rawSong.albumArtistNames, + rawSong.albumArtistSortNames + ) + val preAlbum = makePreAlbum(rawSong, individualPreArtists, albumPreArtists) + val rawArtists = + individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) } + val rawGenres = + makePreGenres(rawSong).ifEmpty { listOf(unknownPreGenre()) } + val uri = need(rawSong, "uri", rawSong.mediaStoreId).toAudioUri() + return PreSong( + musicBrainzId = rawSong.musicBrainzId?.toUuidOrNull(), + name = nameFactory.parse(need(rawSong, "name", rawSong.name), rawSong.sortName), + rawName = rawSong.name, + track = rawSong.track, + disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }, + date = rawSong.date, + uri = uri, + cover = inferCover(rawSong), + path = need(rawSong, "path", rawSong.path), + mimeType = MimeType( + need(rawSong, "mime type", rawSong.extensionMimeType), + null + ), + size = need(rawSong, "size", rawSong.size), + durationMs = need(rawSong, "duration", rawSong.durationMs), + replayGainAdjustment = ReplayGainAdjustment( + rawSong.replayGainTrackAdjustment, + rawSong.replayGainAlbumAdjustment, + ), + dateAdded = need(rawSong, "date added", rawSong.dateAdded), + preAlbum = preAlbum, + preArtists = rawArtists, + preGenres = rawGenres + ) + } + + private fun need(rawSong: RawSong, what: String, value: T?) = + requireNotNull(value) { "Invalid $what for song ${rawSong.path}: No $what" } + + private fun inferCover(rawSong: RawSong): Cover { + val uri = need(rawSong, "uri", rawSong.mediaStoreId).toAudioUri() + return rawSong.coverPerceptualHash?.let { + Cover.Embedded( + requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" } + .toSongCoverUri(), + uri, + it) + } ?: Cover.External(requireNotNull(rawSong.albumMediaStoreId).toAlbumCoverUri()) + } + + private fun makePreAlbum( + rawSong: RawSong, + individualPreArtists: List, + albumPreArtists: List + ): PreAlbum { + val rawAlbumName = need(rawSong, "album name", rawSong.albumName) + return PreAlbum( + musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(), + name = nameFactory.parse(rawAlbumName, rawSong.albumSortName), + rawName = rawAlbumName, + releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)) + ?: ReleaseType.Album(null), + preArtists = + albumPreArtists + .ifEmpty { individualPreArtists } + .ifEmpty { listOf(unknownPreArtist()) }) + } + + private fun makePreArtists( + rawMusicBrainzIds: List, + rawNames: List, + rawSortNames: List + ): List { + val musicBrainzIds = separators.split(rawMusicBrainzIds) + val names = separators.split(rawNames) + val sortNames = separators.split(rawSortNames) + return names + .mapIndexed { i, name -> + makePreArtist( + musicBrainzIds.getOrNull(i), + name, + sortNames.getOrNull(i) + ) + } + + } + + private fun makePreArtist( + musicBrainzId: String?, + rawName: String?, + sortName: String? + ): PreArtist { + val name = + rawName?.let { nameFactory.parse(it, sortName) } ?: Name.Unknown(R.string.def_artist) + val musicBrainzId = musicBrainzId?.toUuidOrNull() + return PreArtist(musicBrainzId, name, rawName) + } + + private fun unknownPreArtist() = + PreArtist(null, Name.Unknown(R.string.def_artist), null) + + private fun makePreGenres(rawSong: RawSong): List { + val genreNames = + rawSong.genreNames.parseId3GenreNames() ?: separators.split(rawSong.genreNames) + return genreNames.map { makePreGenre(it) } + } + + private fun makePreGenre(rawName: String?) = + PreGenre(rawName?.let { nameFactory.parse(it, null) } ?: Name.Unknown(R.string.def_genre), + rawName) + + private fun unknownPreGenre() = + PreGenre(Name.Unknown(R.string.def_genre), null) + +} \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt index 289e4e59f..eaa38208e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/info/Name.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Auxio Project + * Copyright (c) 2023 Auxio Prct * Name.kt is part of Auxio. * * This program is free software: you can redistribute it and/or modify