diff --git a/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt index 69ceec81a..50a8d33e4 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/decision/DetailDecisionViewModel.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.device.DeviceLibrary +import org.oxycblt.auxio.music.model.DeviceLibrary import timber.log.Timber as L /** 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 2ef61e650..77a6c4ead 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield -import org.oxycblt.auxio.music.device.DeviceLibrary +import org.oxycblt.auxio.music.model.DeviceLibrary import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.stack.Indexer 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 deleted file mode 100644 index 4287c5aa2..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceLibrary.kt +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * DeviceLibrary.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.music.device - -import android.content.Context -import android.net.Uri -import java.util.UUID -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.music.MusicRepository -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.info.Name -import org.oxycblt.auxio.music.metadata.Separators -import org.oxycblt.auxio.music.stack.fs.Path -import org.oxycblt.auxio.util.unlikelyToBeNull -import timber.log.Timber as L - -/** - * Organized music library information obtained from device storage. - * - * This class allows for the creation of a well-formed music library graph from raw song - * information. Instances are immutable. It's generally not expected to create this yourself and - * instead use [MusicRepository]. - * - * @author Alexander Capehart - */ -interface DeviceLibrary { - /** All [Song]s in this [DeviceLibrary]. */ - val songs: Collection - - /** All [Album]s in this [DeviceLibrary]. */ - val albums: Collection - - /** All [Artist]s in this [DeviceLibrary]. */ - val artists: Collection - - /** All [Genre]s in this [DeviceLibrary]. */ - val genres: Collection - - /** - * Find a [Song] instance corresponding to the given [Music.UID]. - * - * @param uid The [Music.UID] to search for. - * @return The corresponding [Song], or null if one was not found. - */ - fun findSong(uid: Music.UID): Song? - - /** - * Find a [Song] instance corresponding to the given Intent.ACTION_VIEW [Uri]. - * - * @param context [Context] required to analyze the [Uri]. - * @param uri [Uri] to search for. - * @return A [Song] corresponding to the given [Uri], or null if one could not be found. - */ - fun findSongForUri(context: Context, uri: Uri): Song? - - /** - * Find a [Song] instance corresponding to the given [Path]. - * - * @param path [Path] to search for. - * @return A [Song] corresponding to the given [Path], or null if one could not be found. - */ - fun findSongByPath(path: Path): Song? - - /** - * Find a [Album] instance corresponding to the given [Music.UID]. - * - * @param uid The [Music.UID] to search for. - * @return The corresponding [Album], or null if one was not found. - */ - fun findAlbum(uid: Music.UID): Album? - - /** - * Find a [Artist] instance corresponding to the given [Music.UID]. - * - * @param uid The [Music.UID] to search for. - * @return The corresponding [Artist], or null if one was not found. - */ - fun findArtist(uid: Music.UID): Artist? - - /** - * Find a [Genre] instance corresponding to the given [Music.UID]. - * - * @param uid The [Music.UID] to search for. - * @return The corresponding [Genre], or null if one was not found. - */ - fun findGenre(uid: Music.UID): Genre? - - /** Constructs a [DeviceLibrary] implementation in an asynchronous manner. */ - interface Factory { - /** - * Creates a new [DeviceLibrary] instance asynchronously based on the incoming stream of - * [RawSong] instances. - * - * @param rawSongs A stream of [RawSong] instances to process. - * @param processedSongs A stream of [RawSong] instances that will have been processed by - * the instance. - */ - suspend fun create( - rawSongs: Flow, - onSongProcessed: () -> Unit, - separators: Separators, - nameFactory: Name.Known.Factory - ): DeviceLibraryImpl - } -} - -class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory { - override suspend fun create( - rawSongs: Flow, - onSongProcessed: () -> Unit, - separators: Separators, - nameFactory: Name.Known.Factory - ): DeviceLibraryImpl { - val songGrouping = mutableMapOf() - val albumGrouping = mutableMapOf>>() - val artistGrouping = mutableMapOf>>() - val genreGrouping = mutableMapOf>() - - // All music information is grouped as it is indexed by other components. - rawSongs.collect { rawSong -> - val song = SongImpl(rawSong, nameFactory, separators) - // At times the indexer produces duplicate songs, try to filter these. Comparing by - // UID is sufficient for something like this, and also prevents collisions from - // causing severe issues elsewhere. - if (songGrouping.containsKey(song.uid)) { - L.w( - "Duplicate song found: ${song.path} " + - "collides with ${unlikelyToBeNull(songGrouping[song.uid]).path}") - onSongProcessed() - return@collect - } - songGrouping[song.uid] = song - - // Group the new song into an album. - appendToMusicBrainzIdTree(song, song.rawAlbum, albumGrouping) { old, new -> - compareSongTracks(old, new) - } - // Group the song into each of it's artists. - for (rawArtist in song.rawArtists) { - appendToMusicBrainzIdTree(song, rawArtist, artistGrouping) { old, new -> - // Artist information from earlier dates is prioritized, as it is less likely to - // change with the addition of new tracks. Fall back to the name otherwise. - check(old is SongImpl) // This should always be the case. - compareSongDates(old, new) - } - } - - // Group the song into each of it's genres. - for (rawGenre in song.rawGenres) { - appendToNameTree(song, rawGenre, genreGrouping) { old, new -> new.name < old.name } - } - - onSongProcessed() - } - - // Now that all songs are processed, also process albums and group them into their - // respective artists. - pruneMusicBrainzIdTree(albumGrouping) { old, new -> compareSongTracks(old, new) } - val albums = flattenMusicBrainzIdTree(albumGrouping) { AlbumImpl(it, nameFactory) } - for (album in albums) { - for (rawArtist in album.rawArtists) { - appendToMusicBrainzIdTree(album, rawArtist, artistGrouping) { old, new -> - when (old) { - // Immediately replace any songs that initially held the priority position. - is SongImpl -> true - is AlbumImpl -> { - compareAlbumDates(old, new) - } - else -> throw IllegalStateException() - } - } - } - } - - // Artists and genres do not need to be grouped and can be processed immediately. - pruneMusicBrainzIdTree(artistGrouping) { old, new -> - when { - // Immediately replace any songs that initially held the priority position. - old is SongImpl && new is AlbumImpl -> true - old is AlbumImpl && new is SongImpl -> false - old is SongImpl && new is SongImpl -> { - compareSongDates(old, new) - } - old is AlbumImpl && new is AlbumImpl -> { - compareAlbumDates(old, new) - } - else -> throw IllegalStateException() - } - } - val artists = flattenMusicBrainzIdTree(artistGrouping) { ArtistImpl(it, nameFactory) } - val genres = flattenNameTree(genreGrouping) { GenreImpl(it, nameFactory) } - - return DeviceLibraryImpl(songGrouping.values.toSet(), albums, artists, genres) - } - - private inline fun appendToNameTree( - music: N, - raw: R, - tree: MutableMap>, - prioritize: (old: O, new: N) -> Boolean, - ) { - val nameKey = raw.name?.lowercase() - val body = tree[nameKey] - if (body != null) { - body.music.add(music) - if (prioritize(body.raw.src, music)) { - body.raw = PrioritizedRaw(raw, music) - } - } else { - // Need to initialize this grouping. - tree[nameKey] = Grouping(PrioritizedRaw(raw, music), mutableSetOf(music)) - } - } - - private inline fun flattenNameTree( - tree: MutableMap>, - map: (Grouping) -> P - ): Set

= tree.values.mapTo(mutableSetOf()) { map(it) } - - private inline fun appendToMusicBrainzIdTree( - music: N, - raw: R, - tree: MutableMap>>, - prioritize: (old: O, new: N) -> Boolean, - ) { - val nameKey = raw.name?.lowercase() - val musicBrainzIdGroups = tree[nameKey] - if (musicBrainzIdGroups != null) { - val body = musicBrainzIdGroups[raw.musicBrainzId] - if (body != null) { - body.music.add(music) - if (prioritize(body.raw.src, music)) { - body.raw = PrioritizedRaw(raw, music) - } - } else { - // Need to initialize this grouping. - musicBrainzIdGroups[raw.musicBrainzId] = - Grouping(PrioritizedRaw(raw, music), mutableSetOf(music)) - } - } else { - // Need to initialize this grouping. - tree[nameKey] = - mutableMapOf( - raw.musicBrainzId to Grouping(PrioritizedRaw(raw, music), mutableSetOf(music))) - } - } - - private inline fun pruneMusicBrainzIdTree( - tree: MutableMap>>, - prioritize: (old: M, new: M) -> Boolean - ) { - for ((_, musicBrainzIdGroups) in tree) { - var nullGroup = musicBrainzIdGroups[null] - if (nullGroup == null) { - // Full MusicBrainz ID tagging. Nothing to do. - continue - } - // Only partial MusicBrainz ID tagging. For the sake of basic sanity, just - // collapse all of them into the null group. - // TODO: More advanced heuristics eventually (tm) - musicBrainzIdGroups - .filter { it.key != null } - .forEach { - val (_, group) = it - nullGroup.music.addAll(group.music) - if (prioritize(group.raw.src, nullGroup.raw.src)) { - nullGroup.raw = group.raw - } - musicBrainzIdGroups.remove(it.key) - } - } - } - - private inline fun flattenMusicBrainzIdTree( - tree: MutableMap>>, - map: (Grouping) -> T - ): Set { - val result = mutableSetOf() - for ((_, musicBrainzIdGroups) in tree) { - for (group in musicBrainzIdGroups.values) { - result += map(group) - } - } - return result - } - - private fun compareSongTracks(old: SongImpl, new: SongImpl) = - new.track != null && - (old.track == null || - new.track < old.track || - (new.track == old.track && new.name < old.name)) - - private fun compareAlbumDates(old: AlbumImpl, new: AlbumImpl) = - new.dates != null && - (old.dates == null || - new.dates < old.dates || - (new.dates == old.dates && new.name < old.name)) - - private fun compareSongDates(old: SongImpl, new: SongImpl) = - new.date != null && - (old.date == null || - new.date < old.date || - (new.date == old.date && new.name < old.name)) -} - -// TODO: Avoid redundant data creation - -class DeviceLibraryImpl( - override val songs: Collection, - override val albums: Collection, - override val artists: Collection, - override val genres: Collection -) : DeviceLibrary { - // Use a mapping to make finding information based on it's UID much faster. - private val songUidMap = buildMap { songs.forEach { put(it.uid, it.finalize()) } } - // private val songPathMap = buildMap { songs.forEach { put(it.path, it) } } - private val albumUidMap = buildMap { albums.forEach { put(it.uid, it.finalize()) } } - private val artistUidMap = buildMap { artists.forEach { put(it.uid, it.finalize()) } } - private val genreUidMap = buildMap { genres.forEach { put(it.uid, it.finalize()) } } - - // All other music is built from songs, so comparison only needs to check songs. - override fun equals(other: Any?) = other is DeviceLibrary && other.songs == songs - - override fun hashCode() = songs.hashCode() - - override fun toString() = - "DeviceLibrary(songs=${songs.size}, albums=${albums.size}, " + - "artists=${artists.size}, genres=${genres.size})" - - override fun findSong(uid: Music.UID): Song? = songUidMap[uid] - - override fun findAlbum(uid: Music.UID): Album? = albumUidMap[uid] - - override fun findArtist(uid: Music.UID): Artist? = artistUidMap[uid] - - override fun findGenre(uid: Music.UID): Genre? = genreUidMap[uid] - - override fun findSongByPath(path: Path) = null - - override fun findSongForUri(context: Context, uri: Uri) = null - // context.contentResolverSafe.useQuery( - // uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor -> - // cursor.moveToFirst() - // // We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a - // // song. Do what we can to hopefully find the song the user wanted to open. - // val displayName = - // - // cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) - // val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) - // songs.find { it.path.name == displayName && it.size == size } - // } -} 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 deleted file mode 100644 index 17cec9bf2..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ /dev/null @@ -1,604 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * DeviceMusicImpl.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.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 -import org.oxycblt.auxio.music.Artist -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.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.stack.extractor.parseId3GenreNames -import org.oxycblt.auxio.music.stack.fs.MimeType -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 - -/** - * 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. - * @author Alexander Capehart (OxygenCobalt) - */ -class SongImpl( - private val rawSong: RawSong, - private val nameFactory: Name.Known.Factory, - private val separators: Separators -) : Song { - 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) } - ?: 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(rawSong.track) - update(rawSong.disc) - - update(rawSong.artistNames) - update(rawSong.albumArtistNames) - } - override val name = - nameFactory.parse( - requireNotNull(rawSong.name) { "Invalid raw ${rawSong.file.path}: No title" }, - rawSong.sortName) - - override val track = rawSong.track - override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) } - override val date = rawSong.date - override val uri = rawSong.file.uri - override val path = rawSong.file.path - override val mimeType = MimeType(fromExtension = rawSong.file.mimeType, fromFormat = null) - override val size = - requireNotNull(rawSong.file.size) { "Invalid raw ${rawSong.file.path}: No size" } - override val durationMs = - requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.file.path}: No duration" } - override val replayGainAdjustment = - ReplayGainAdjustment( - track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment) - - // TODO: See what we want to do with date added now that we can't get it anymore. - override val dateAdded = - requireNotNull(rawSong.file.lastModified) { - "Invalid raw ${rawSong.file.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 - - // TODO: Rebuild cover system - override val cover = Cover.External(rawSong.file.uri) - - /** - * 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( - musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(), - name = - requireNotNull(rawSong.albumName) { - "Invalid raw ${rawSong.file.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() - } - - 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 - - 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 - - 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) } - ?: 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 }) - } - 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 - - private val _artists = mutableListOf() - override val artists: List - get() = _artists - - 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() - } - - 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 && - rawAlbum == other.rawAlbum && - nameFactory == other.nameFactory && - 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) - } - - /** - * Perform final validation and organization on this instance. - * - * @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 - } -} - -/** - * Library-backed implementation of [Artist]. - * - * @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 ArtistImpl( - grouping: Grouping, - private val nameFactory: Name.Known.Factory -) : Artist { - private val rawArtist = grouping.raw.inner - - override val uid = - // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. - rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ARTISTS, it) } - ?: Music.UID.auxio(MusicType.ARTISTS) { update(rawArtist.name) } - override val name = - rawArtist.name?.let { nameFactory.parse(it, rawArtist.sortName) } - ?: Name.Unknown(R.string.def_artist) - - override val songs: Set - override val explicitAlbums: Set - override val implicitAlbums: Set - override val durationMs: Long? - override val cover: ParentCover - - override lateinit var genres: List - - private var hashCode = uid.hashCode() - - init { - val distinctSongs = mutableSetOf() - val albumMap = mutableMapOf() - - for (music in grouping.music) { - when (music) { - is SongImpl -> { - music.link(this) - distinctSongs.add(music) - if (albumMap[music.album] == null) { - albumMap[music.album] = false - } - } - is AlbumImpl -> { - music.link(this) - albumMap[music] = true - } - else -> error("Unexpected input music $music in $name ${music::class.simpleName}") - } - } - - songs = distinctSongs - val albums = albumMap.keys - explicitAlbums = albums.filterTo(mutableSetOf()) { albumMap[it] == true } - implicitAlbums = albums.filterNotTo(mutableSetOf()) { albumMap[it] == true } - durationMs = songs.sumOf { it.durationMs }.positiveOrNull() - - val singleCover = - when (val src = grouping.raw.src) { - is SongImpl -> src.cover - is AlbumImpl -> src.cover.single - else -> error("Unexpected input source $src in $name ${src::class.simpleName}") - } - cover = ParentCover.from(singleCover, songs) - - hashCode = 31 * hashCode + rawArtist.hashCode() - hashCode = 31 * hashCode + nameFactory.hashCode() - hashCode = 31 * hashCode + 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 && - 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. - * - * @return This instance upcasted to [Artist]. - */ - fun finalize(): Artist { - // There are valid artist configurations: - // 1. No songs, no implicit albums, some explicit albums - // 2. Some songs, no implicit albums, some explicit albums - // 3. Some songs, some implicit albums, no implicit albums - // 4. Some songs, some implicit albums, some explicit albums - // I'm pretty sure the latter check could be reduced to just explicitAlbums.isNotEmpty, - // but I can't be 100% certain. - check(songs.isNotEmpty() || (implicitAlbums.size + explicitAlbums.size) > 0) { - "Malformed artist $name: Empty" - } - genres = - Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) - .genres(songs.flatMapTo(mutableSetOf()) { it.genres }) - .sortedByDescending { genre -> songs.count { it.genres.contains(genre) } } - return this - } -} - -/** - * Library-backed implementation of [Genre]. - * - * @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 GenreImpl( - grouping: Grouping, - private val nameFactory: Name.Known.Factory -) : Genre { - private val rawGenre = grouping.raw.inner - - override val uid = Music.UID.auxio(MusicType.GENRES) { update(rawGenre.name) } - override val name = - rawGenre.name?.let { nameFactory.parse(it, rawGenre.name) } - ?: Name.Unknown(R.string.def_genre) - - override val songs: Set - override val artists: Set - override val durationMs: Long - override val cover: ParentCover - - private var hashCode = uid.hashCode() - - init { - val distinctArtists = mutableSetOf() - var totalDuration = 0L - - for (song in grouping.music) { - song.link(this) - distinctArtists.addAll(song.artists) - totalDuration += song.durationMs - } - - songs = grouping.music - artists = distinctArtists - durationMs = totalDuration - - cover = ParentCover.from(grouping.raw.src.cover, songs) - - hashCode = 31 * hashCode + rawGenre.hashCode() - hashCode = 31 * hashCode + nameFactory.hashCode() - hashCode = 31 * hashCode + songs.hashCode() - } - - override fun hashCode() = hashCode - - override fun equals(other: Any?) = - other is GenreImpl && - 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. - * - * @return This instance upcasted to [Genre]. - */ - fun finalize(): Genre { - check(songs.isNotEmpty()) { "Malformed genre $name: Empty" } - return this - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt deleted file mode 100644 index 7f7461f75..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * RawMusic.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.music.device - -import java.util.UUID -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.info.Date -import org.oxycblt.auxio.music.info.ReleaseType -import org.oxycblt.auxio.music.stack.fs.DeviceFile - -/** - * Raw information about a [SongImpl] obtained from the filesystem/Extractor instances. - * - * @author Alexander Capehart (OxygenCobalt) - */ -data class RawSong( - val file: DeviceFile, - /** @see Song.durationMs */ - var durationMs: Long? = null, - /** @see Song.replayGainAdjustment */ - var replayGainTrackAdjustment: Float? = null, - /** @see Song.replayGainAdjustment */ - var replayGainAlbumAdjustment: Float? = null, - /** @see Music.UID */ - var musicBrainzId: String? = null, - /** @see Music.name */ - var name: String? = null, - /** @see Music.name */ - var sortName: String? = null, - /** @see Song.track */ - var track: Int? = null, - /** @see Song.disc */ - var disc: Int? = null, - /** @See Song.disc */ - var subtitle: String? = null, - /** @see Song.date */ - var date: Date? = null, - /** @see RawAlbum.musicBrainzId */ - var albumMusicBrainzId: String? = null, - /** @see RawAlbum.name */ - var albumName: String? = null, - /** @see RawAlbum.sortName */ - var albumSortName: String? = null, - /** @see RawAlbum.releaseType */ - var releaseTypes: List = listOf(), - /** @see RawArtist.musicBrainzId */ - var artistMusicBrainzIds: List = listOf(), - /** @see RawArtist.name */ - var artistNames: List = listOf(), - /** @see RawArtist.sortName */ - var artistSortNames: List = listOf(), - /** @see RawArtist.musicBrainzId */ - var albumArtistMusicBrainzIds: List = listOf(), - /** @see RawArtist.name */ - var albumArtistNames: List = listOf(), - /** @see RawArtist.sortName */ - var albumArtistSortNames: List = listOf(), - /** @see RawGenre.name */ - var genreNames: List = listOf() -) - -/** - * Raw information about an [AlbumImpl] obtained from the component [SongImpl] instances. - * - * @author Alexander Capehart (OxygenCobalt) - */ -data class RawAlbum( - /** @see Music.uid */ - override val musicBrainzId: UUID?, - /** @see Music.name */ - override val name: String, - /** @see Music.name */ - val sortName: String?, - /** @see Album.releaseType */ - val releaseType: ReleaseType?, - /** @see RawArtist.name */ - val rawArtists: List -) : MusicBrainzGroupable - -/** - * Raw information about an [ArtistImpl] obtained from the component [SongImpl] and [AlbumImpl] - * instances. - * - * @author Alexander Capehart (OxygenCobalt) - */ -data class RawArtist( - /** @see Music.UID */ - override val musicBrainzId: UUID? = null, - /** @see Music.name */ - override val name: String? = null, - /** @see Music.name */ - val sortName: String? = null -) : MusicBrainzGroupable - -/** - * Raw information about a [GenreImpl] obtained from the component [SongImpl] instances. - * - * @author Alexander Capehart (OxygenCobalt) - */ -data class RawGenre( - /** @see Music.name */ - override val name: String? = null -) : NameGroupable - -interface NameGroupable { - val name: String? -} - -interface MusicBrainzGroupable : NameGroupable { - val musicBrainzId: UUID? -} - -/** - * Represents grouped music information and the prioritized raw information to eventually derive a - * [Music] implementation instance from. - * - * @param raw The current [PrioritizedRaw] that will be used for the finalized music information. - * @param music The child [Music] instances of the music information to be created. - */ -data class Grouping(var raw: PrioritizedRaw, val music: MutableSet) - -/** - * Represents a [RawAlbum], [RawArtist], or [RawGenre] specifically chosen to create a [Music] - * instance from due to it being the most likely source of truth. - * - * @param inner The raw music instance that will be used. - * @param src The [Music] instance that the raw information was derived from. - */ -data class PrioritizedRaw(val inner: R, val src: M) 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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt b/app/src/main/java/org/oxycblt/auxio/music/model/DeviceModule.kt similarity index 81% rename from app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt rename to app/src/main/java/org/oxycblt/auxio/music/model/DeviceModule.kt index 85e8e511e..d435c200e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/model/DeviceModule.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.device +package org.oxycblt.auxio.music.model import dagger.Binds import dagger.Module @@ -25,6 +25,8 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) -interface DeviceModule { - @Binds fun deviceLibraryFactory(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory +interface ModelModule { + @Binds fun interpreter(factory: InterpreterImpl): Interpreter + + @Binds fun preparer(preparerImpl: SongInterpreterImpl): SongInterpreter } diff --git a/app/src/main/java/org/oxycblt/auxio/music/model/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/model/DeviceMusicImpl.kt new file mode 100644 index 000000000..061e90e0c --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/model/DeviceMusicImpl.kt @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2023 Auxio Project + * DeviceMusicImpl.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.music.model + +import org.oxycblt.auxio.image.extractor.ParentCover +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 +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicType +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.info.Date +import org.oxycblt.auxio.util.update +import kotlin.math.min + +/** + * 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 + + override val uid = + // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. + 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(preSong.rawName) + update(preSong.preAlbum.rawName) + update(preSong.date) + + update(preSong.track) + update(preSong.disc?.number) + + update(preSong.preArtists.map { it.rawName }) + update(preSong.preAlbum.preArtists.map { it.rawName }) + } + 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) + + private val hashCode = 31 * uid.hashCode() + preSong.hashCode() + + override fun hashCode() = hashCode + + override fun equals(other: Any?) = + other is SongImpl && + uid == other.uid && + preSong == other.preSong + + override fun toString() = "Song(uid=$uid, name=$name)" +} + +/** + * Library-backed implementation of [Album]. + * + * @author Alexander Capehart (OxygenCobalt) + */ +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. + 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(preAlbum.rawName) + update(preAlbum.preArtists.map { it.rawName }) + } + 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 + + override val artists = linkedAlbum.artists.resolve(this) + override val songs = mutableSetOf() + + private var hashCode = 31 * uid.hashCode() + preAlbum.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) + } + hashCode = 31 * hashCode + song.hashCode() + } + + /** + * Perform final validation and organization on this instance. + * + * @return This instance upcasted to [Album]. + */ + fun finalize(): Album { + return this + } +} + +/** + * Library-backed implementation of [Artist]. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class ArtistImpl(private val preArtist: PreArtist) : 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 + + override val songs = mutableSetOf() + + private val albums = mutableSetOf() + private val albumMap = mutableMapOf() + override lateinit var explicitAlbums: Set + override lateinit var implicitAlbums: Set + + + override lateinit var genres: List + + override var durationMs = 0L + override lateinit var cover: ParentCover + + private var hashCode = 31 * uid.hashCode() + preArtist.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 && + songs == other.songs + + override fun toString() = "Artist(uid=$uid, name=$name)" + + fun link(song: SongImpl) { + songs.add(song) + durationMs += song.durationMs + if (albumMap[song.album] == null) { + albumMap[song.album] = false + } + hashCode = 31 * hashCode + song.hashCode() + } + + fun link(album: AlbumImpl) { + albums.add(album) + albumMap[album] = true + } + + /** + * Perform final validation and organization on this instance. + * + * @return This instance upcasted to [Artist]. + */ + fun finalize(): Artist { + // There are valid artist configurations: + // 1. No songs, no implicit albums, some explicit albums + // 2. Some songs, no implicit albums, some explicit albums + // 3. Some songs, some implicit albums, no implicit albums + // 4. Some songs, some implicit albums, some explicit albums + // I'm pretty sure the latter check could be reduced to just explicitAlbums.isNotEmpty, + // but I can't be 100% certain. + check(songs.isNotEmpty() || (implicitAlbums.size + explicitAlbums.size) > 0) { + "Malformed artist $name: Empty" + } + explicitAlbums = albums.filterTo(mutableSetOf()) { albumMap[it] == true } + implicitAlbums = albums.filterNotTo(mutableSetOf()) { albumMap[it] == true } + genres = + Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) + .genres(songs.flatMapTo(mutableSetOf()) { it.genres }) + .sortedByDescending { genre -> songs.count { it.genres.contains(genre) } } + return this + } +} + +/** + * Library-backed implementation of [Genre]. + * + * @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 + + override val songs = mutableSetOf() + override val artists = mutableSetOf() + override var durationMs = 0L + override lateinit var cover: ParentCover + + private var hashCode = uid.hashCode() + + override fun hashCode() = hashCode + + override fun equals(other: Any?) = + other is GenreImpl && + uid == other.uid && + preGenre == other.preGenre && + songs == other.songs + + override fun toString() = "Genre(uid=$uid, name=$name)" + + fun link(song: SongImpl) { + songs.add(song) + artists.addAll(song.artists) + durationMs += song.durationMs + hashCode = 31 * hashCode + song.hashCode() + } + + /** + * Perform final validation and organization on this instance. + * + * @return This instance upcasted to [Genre]. + */ + fun finalize(): Genre { + return this + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/model/Interpretation.kt b/app/src/main/java/org/oxycblt/auxio/music/model/Interpretation.kt new file mode 100644 index 000000000..1902b9e33 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/model/Interpretation.kt @@ -0,0 +1,9 @@ +package org.oxycblt.auxio.music.model + +import org.oxycblt.auxio.music.info.Name +import org.oxycblt.auxio.music.metadata.Separators + +data class Interpretation( + val nameFactory: Name.Known.Factory, + val separators: Separators +) diff --git a/app/src/main/java/org/oxycblt/auxio/music/model/Interpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/model/Interpreter.kt new file mode 100644 index 000000000..e49ba4255 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/model/Interpreter.kt @@ -0,0 +1,67 @@ +package org.oxycblt.auxio.music.model + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import org.oxycblt.auxio.music.stack.AudioFile +import org.oxycblt.auxio.music.stack.PlaylistFile + +interface Interpreter { + suspend fun interpret( + audioFiles: Flow, + playlistFiles: Flow, + interpretation: Interpretation + ): MutableLibrary +} + +class LinkedSong(private val albumLinkedSong: AlbumInterpreter.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 = ArtistInterpreter.LinkedAlbum + +class InterpreterImpl( + private val songInterpreter: SongInterpreter +) : Interpreter { + override suspend fun interpret( + audioFiles: Flow, + playlistFiles: Flow, + interpretation: Interpretation + ): MutableLibrary { + val preSongs = + songInterpreter.prepare(audioFiles, interpretation).flowOn(Dispatchers.Main) + .buffer() + val albumInterpreter = makeAlbumTree() + val artistInterpreter = makeArtistTree() + val genreInterpreter = makeGenreTree() + + val genreLinkedSongs = genreInterpreter.register(preSongs).flowOn(Dispatchers.Main).buffer() + val artistLinkedSongs = + artistInterpreter.register(genreLinkedSongs).flowOn(Dispatchers.Main).buffer() + val albumLinkedSongs = + albumInterpreter.register(artistLinkedSongs).flowOn(Dispatchers.Main) + val linkedSongs = albumLinkedSongs.map { LinkedSong(it) }.toList() + + val genres = genreInterpreter.resolve() + val artists = artistInterpreter.resolve() + val albums = albumInterpreter.resolve() + val songs = linkedSongs.map { SongImpl(it) } + return LibraryImpl(songs, albums, artists, genres) + } + + private fun makeAlbumTree(): AlbumInterpreter { + } + + private fun makeArtistTree(): ArtistInterpreter { + } + + private fun makeGenreTree(): GenreInterpreter { + } + +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt b/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt new file mode 100644 index 000000000..759c04721 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/model/Library.kt @@ -0,0 +1,79 @@ +package org.oxycblt.auxio.music.model + +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.Playlist + +interface Library { + val songs: Collection + val albums: Collection + val artists: Collection + val genres: Collection + val playlists: Collection + + fun findSong(uid: Music.UID): Song? + fun findAlbum(uid: Music.UID): Album? + fun findArtist(uid: Music.UID): Artist? + fun findGenre(uid: Music.UID): Genre? + fun findPlaylist(uid: Music.UID): Playlist? +} + +interface MutableLibrary : Library { + suspend fun createPlaylist(name: String, songs: List): MutableLibrary + suspend fun renamePlaylist(playlist: Playlist, name: String): MutableLibrary + suspend fun addToPlaylist(playlist: Playlist, songs: List): MutableLibrary + suspend fun rewritePlaylist(playlist: Playlist, songs: List): MutableLibrary + suspend fun deletePlaylist(playlist: Playlist): MutableLibrary +} + +class LibraryImpl( + override val songs: Collection, + override val albums: Collection, + override val artists: Collection, + override val genres: Collection +) : MutableLibrary { + override val playlists = emptySet() + + override fun findSong(uid: Music.UID): Song? { + TODO("Not yet implemented") + } + + override fun findAlbum(uid: Music.UID): Album? { + TODO("Not yet implemented") + } + + override fun findArtist(uid: Music.UID): Artist? { + TODO("Not yet implemented") + } + + override fun findGenre(uid: Music.UID): Genre? { + TODO("Not yet implemented") + } + + override fun findPlaylist(uid: Music.UID): Playlist? { + TODO("Not yet implemented") + } + + override suspend fun createPlaylist(name: String, songs: List): MutableLibrary { + TODO("Not yet implemented") + } + + override suspend fun renamePlaylist(playlist: Playlist, name: String): MutableLibrary { + TODO("Not yet implemented") + } + + override suspend fun addToPlaylist(playlist: Playlist, songs: List): MutableLibrary { + TODO("Not yet implemented") + } + + override suspend fun rewritePlaylist(playlist: Playlist, songs: List): MutableLibrary { + TODO("Not yet implemented") + } + + override suspend fun deletePlaylist(playlist: Playlist): MutableLibrary { + TODO("Not yet implemented") + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/model/PreMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/model/PreMusic.kt new file mode 100644 index 000000000..6041dc1a3 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/model/PreMusic.kt @@ -0,0 +1,53 @@ +package org.oxycblt.auxio.music.model + +import android.net.Uri +import org.oxycblt.auxio.image.extractor.Cover +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.stack.fs.MimeType +import org.oxycblt.auxio.music.stack.fs.Path +import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment +import java.util.UUID + +interface PrePlaylist + +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/model/SongInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/model/SongInterpreter.kt new file mode 100644 index 000000000..2198aef07 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/model/SongInterpreter.kt @@ -0,0 +1,145 @@ +package org.oxycblt.auxio.music.model + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.oxycblt.auxio.R +import org.oxycblt.auxio.image.extractor.Cover +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.stack.AudioFile +import org.oxycblt.auxio.music.stack.extractor.parseId3GenreNames +import org.oxycblt.auxio.music.stack.fs.MimeType +import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment +import org.oxycblt.auxio.util.toUuidOrNull + +interface SongInterpreter { + fun prepare(audioFiles: Flow, interpretation: Interpretation): Flow +} + +class SongInterpreterImpl( + private val nameFactory: Name.Known.Factory, + private val separators: Separators +) : SongInterpreter { + override fun prepare(audioFiles: Flow, interpretation: Interpretation) = audioFiles.map { audioFile -> + val individualPreArtists = makePreArtists( + audioFile.artistMusicBrainzIds, + audioFile.artistNames, + audioFile.artistSortNames + ) + val albumPreArtists = makePreArtists( + audioFile.albumArtistMusicBrainzIds, + audioFile.albumArtistNames, + audioFile.albumArtistSortNames + ) + val preAlbum = makePreAlbum(audioFile, individualPreArtists, albumPreArtists) + val rawArtists = + individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) } + val rawGenres = + makePreGenres(audioFile).ifEmpty { listOf(unknownPreGenre()) } + val uri = audioFile.deviceFile.uri + PreSong( + musicBrainzId = audioFile.musicBrainzId?.toUuidOrNull(), + name = nameFactory.parse(need(audioFile, "name", audioFile.name), audioFile.sortName), + rawName = audioFile.name, + track = audioFile.track, + disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) }, + date = audioFile.date, + uri = uri, + cover = inferCover(audioFile), + path = need(audioFile, "path", audioFile.deviceFile.path), + mimeType = MimeType( + need(audioFile, "mime type", audioFile.deviceFile.mimeType), + null + ), + size = audioFile.deviceFile.size, + durationMs = need(audioFile, "duration", audioFile.durationMs), + replayGainAdjustment = ReplayGainAdjustment( + audioFile.replayGainTrackAdjustment, + audioFile.replayGainAlbumAdjustment, + ), + // TODO: Figure out what to do with date added + dateAdded = audioFile.deviceFile.lastModified, + preAlbum = preAlbum, + preArtists = rawArtists, + preGenres = rawGenres + ) + } + + private fun need(audioFile: AudioFile, what: String, value: T?) = + requireNotNull(value) { "Invalid $what for song ${audioFile.deviceFile.path}: No $what" } + + private fun inferCover(audioFile: AudioFile): Cover { + return Cover.Embedded( + audioFile.deviceFile.uri, + audioFile.deviceFile.uri, + "" + ) + } + + private fun makePreAlbum( + audioFile: AudioFile, + individualPreArtists: List, + albumPreArtists: List + ): PreAlbum { + val rawAlbumName = need(audioFile, "album name", audioFile.albumName) + return PreAlbum( + musicBrainzId = audioFile.albumMusicBrainzId?.toUuidOrNull(), + name = nameFactory.parse(rawAlbumName, audioFile.albumSortName), + rawName = rawAlbumName, + releaseType = ReleaseType.parse(separators.split(audioFile.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(audioFile: AudioFile): List { + val genreNames = + audioFile.genreNames.parseId3GenreNames() ?: separators.split(audioFile.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/model/Trees.kt b/app/src/main/java/org/oxycblt/auxio/music/model/Trees.kt new file mode 100644 index 000000000..8fec57fa2 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/model/Trees.kt @@ -0,0 +1,44 @@ +package org.oxycblt.auxio.music.model + +import kotlinx.coroutines.flow.Flow + + +interface AlbumInterpreter { + suspend fun register(linkedSongs: Flow): Flow + fun resolve(): Collection + + data class LinkedSong( + val linkedArtistSong: ArtistInterpreter.LinkedSong, + val album: Linked + ) +} + +interface ArtistInterpreter { + suspend fun register(preSong: Flow): Flow + fun resolve(): Collection + + data class LinkedSong( + val linkedGenreSong: GenreInterpreter.LinkedSong, + val linkedAlbum: LinkedAlbum, + val artists: Linked, SongImpl> + ) + + data class LinkedAlbum( + val preAlbum: PreAlbum, + val artists: Linked, AlbumImpl> + ) +} + +interface GenreInterpreter { + suspend fun register(preSong: Flow): Flow + 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/stack/Files.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/Files.kt new file mode 100644 index 000000000..82e0b4eea --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/Files.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 Auxio Project + * SongInterpreter.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.music.stack + +import android.net.Uri +import org.oxycblt.auxio.music.model.SongImpl +import org.oxycblt.auxio.music.info.Date +import org.oxycblt.auxio.music.stack.fs.Path + +data class DeviceFile( + val uri: Uri, + val mimeType: String, + val path: Path, + val size: Long, + val lastModified: Long +) + +/** + * Raw information about a [SongImpl] obtained from the filesystem/Extractor instances. + * + * @author Alexander Capehart (OxygenCobalt) + */ +data class AudioFile( + val deviceFile: DeviceFile, + var durationMs: Long? = null, + var replayGainTrackAdjustment: Float? = null, + var replayGainAlbumAdjustment: Float? = null, + var musicBrainzId: String? = null, + var name: String? = null, + var sortName: String? = null, + var track: Int? = null, + var disc: Int? = null, + var subtitle: String? = null, + var date: Date? = null, + var albumMusicBrainzId: String? = null, + var albumName: String? = null, + var albumSortName: String? = null, + var releaseTypes: List = listOf(), + var artistMusicBrainzIds: List = listOf(), + var artistNames: List = listOf(), + var artistSortNames: List = listOf(), + var albumArtistMusicBrainzIds: List = listOf(), + var albumArtistNames: List = listOf(), + var albumArtistSortNames: List = listOf(), + var genreNames: List = listOf() +) + +interface PlaylistFile { + val name: String +} \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt index d8342544d..1fe6275a8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/Indexer.kt @@ -28,32 +28,29 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.shareIn -import org.oxycblt.auxio.music.device.DeviceLibrary -import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.metadata.Separators +import org.oxycblt.auxio.music.model.Interpretation +import org.oxycblt.auxio.music.model.Interpreter +import org.oxycblt.auxio.music.model.MutableLibrary import org.oxycblt.auxio.music.stack.cache.TagCache import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor import org.oxycblt.auxio.music.stack.extractor.TagResult -import org.oxycblt.auxio.music.stack.fs.DeviceFile import org.oxycblt.auxio.music.stack.fs.DeviceFiles -import org.oxycblt.auxio.music.user.MutableUserLibrary -import org.oxycblt.auxio.music.user.UserLibrary interface Indexer { suspend fun run( uris: List, - separators: Separators, - nameFactory: Name.Known.Factory - ): LibraryResult + interpretation: Interpretation + ): MutableLibrary } -data class LibraryResult(val deviceLibrary: DeviceLibrary, val userLibrary: MutableUserLibrary) class IndexerImpl @Inject @@ -61,39 +58,34 @@ constructor( private val deviceFiles: DeviceFiles, private val tagCache: TagCache, private val tagExtractor: ExoPlayerTagExtractor, - private val deviceLibraryFactory: DeviceLibrary.Factory, - private val userLibraryFactory: UserLibrary.Factory + private val interpreter: Interpreter ) : Indexer { override suspend fun run( uris: List, - separators: Separators, - nameFactory: Name.Known.Factory + interpretation: Interpretation ) = coroutineScope { val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer() val tagRead = tagCache.read(deviceFiles).flowOn(Dispatchers.IO).buffer() - val (cacheFiles, cacheSongs) = tagRead.split() + val (cacheFiles, cacheSongs) = tagRead.results() val tagExtractor = tagExtractor.process(cacheFiles).flowOn(Dispatchers.IO).buffer() - val (_, extractorSongs) = tagExtractor.split() + val (_, extractorSongs) = tagExtractor.results() val sharedExtractorSongs = extractorSongs.shareIn( CoroutineScope(Dispatchers.Main), started = SharingStarted.WhileSubscribed(), replay = Int.MAX_VALUE) val tagWrite = - async(Dispatchers.IO) { tagCache.write(merge(cacheSongs, sharedExtractorSongs)) } - val rawPlaylists = async(Dispatchers.IO) { userLibraryFactory.query() } - val deviceLibrary = - deviceLibraryFactory.create( - merge(cacheSongs, sharedExtractorSongs), {}, separators, nameFactory) - val userLibrary = - userLibraryFactory.create(rawPlaylists.await(), deviceLibrary, nameFactory) + async(Dispatchers.IO) { tagCache.write(sharedExtractorSongs) } + val library = async(Dispatchers.Main) { interpreter.interpret( + merge(cacheSongs, sharedExtractorSongs), emptyFlow(), interpretation + )} tagWrite.await() - LibraryResult(deviceLibrary, userLibrary) + library.await() } - private fun Flow.split(): Pair, Flow> { + private fun Flow.results(): Pair, Flow> { val files = filterIsInstance().map { it.file } - val songs = filterIsInstance().map { it.rawSong } + val songs = filterIsInstance().map { it.audioFile } return files to songs } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagCache.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagCache.kt index 874e418e6..9ad1c2e12 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagCache.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagCache.kt @@ -21,14 +21,14 @@ package org.oxycblt.auxio.music.stack.cache import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.transform -import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.music.stack.AudioFile import org.oxycblt.auxio.music.stack.extractor.TagResult -import org.oxycblt.auxio.music.stack.fs.DeviceFile +import org.oxycblt.auxio.music.stack.DeviceFile interface TagCache { fun read(files: Flow): Flow - suspend fun write(rawSongs: Flow) + suspend fun write(rawSongs: Flow) } class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache { @@ -36,15 +36,15 @@ class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache { files.transform { file -> val tags = tagDao.selectTags(file.uri.toString(), file.lastModified) if (tags != null) { - val rawSong = RawSong(file = file) - tags.copyToRaw(rawSong) - TagResult.Hit(rawSong) + val audioFile = AudioFile(deviceFile = file) + tags.copyToRaw(audioFile) + TagResult.Hit(audioFile) } else { TagResult.Miss(file) } } - override suspend fun write(rawSongs: Flow) { + override suspend fun write(rawSongs: Flow) { rawSongs.collect { rawSong -> tagDao.updateTags(Tags.fromRaw(rawSong)) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagDatabase.kt index fc52a9806..1bfbefd48 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/cache/TagDatabase.kt @@ -28,7 +28,7 @@ import androidx.room.Query import androidx.room.RoomDatabase import androidx.room.TypeConverter import androidx.room.TypeConverters -import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.music.stack.AudioFile import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.stack.extractor.correctWhitespace import org.oxycblt.auxio.music.stack.extractor.splitEscaped @@ -50,84 +50,84 @@ interface TagDao { @TypeConverters(Tags.Converters::class) data class Tags( /** - * The Uri of the [RawSong]'s audio file, obtained from SAF. This should ideally be a black box + * The Uri of the [AudioFile]'s audio file, obtained from SAF. This should ideally be a black box * only used for comparison. */ @PrimaryKey val uri: String, - /** The latest date the [RawSong]'s audio file was modified, as a unix epoch timestamp. */ + /** The latest date the [AudioFile]'s audio file was modified, as a unix epoch timestamp. */ val dateModified: Long, - /** @see RawSong */ + /** @see AudioFile */ val durationMs: Long, - /** @see RawSong.replayGainTrackAdjustment */ + /** @see AudioFile.replayGainTrackAdjustment */ val replayGainTrackAdjustment: Float? = null, - /** @see RawSong.replayGainAlbumAdjustment */ + /** @see AudioFile.replayGainAlbumAdjustment */ val replayGainAlbumAdjustment: Float? = null, - /** @see RawSong.musicBrainzId */ + /** @see AudioFile.musicBrainzId */ var musicBrainzId: String? = null, - /** @see RawSong.name */ + /** @see AudioFile.name */ var name: String, - /** @see RawSong.sortName */ + /** @see AudioFile.sortName */ var sortName: String? = null, - /** @see RawSong.track */ + /** @see AudioFile.track */ var track: Int? = null, - /** @see RawSong.name */ + /** @see AudioFile.name */ var disc: Int? = null, - /** @See RawSong.subtitle */ + /** @See AudioFile.subtitle */ var subtitle: String? = null, - /** @see RawSong.date */ + /** @see AudioFile.date */ var date: Date? = null, - /** @see RawSong.albumMusicBrainzId */ + /** @see AudioFile.albumMusicBrainzId */ var albumMusicBrainzId: String? = null, - /** @see RawSong.albumName */ + /** @see AudioFile.albumName */ var albumName: String, - /** @see RawSong.albumSortName */ + /** @see AudioFile.albumSortName */ var albumSortName: String? = null, - /** @see RawSong.releaseTypes */ + /** @see AudioFile.releaseTypes */ var releaseTypes: List = listOf(), - /** @see RawSong.artistMusicBrainzIds */ + /** @see AudioFile.artistMusicBrainzIds */ var artistMusicBrainzIds: List = listOf(), - /** @see RawSong.artistNames */ + /** @see AudioFile.artistNames */ var artistNames: List = listOf(), - /** @see RawSong.artistSortNames */ + /** @see AudioFile.artistSortNames */ var artistSortNames: List = listOf(), - /** @see RawSong.albumArtistMusicBrainzIds */ + /** @see AudioFile.albumArtistMusicBrainzIds */ var albumArtistMusicBrainzIds: List = listOf(), - /** @see RawSong.albumArtistNames */ + /** @see AudioFile.albumArtistNames */ var albumArtistNames: List = listOf(), - /** @see RawSong.albumArtistSortNames */ + /** @see AudioFile.albumArtistSortNames */ var albumArtistSortNames: List = listOf(), - /** @see RawSong.genreNames */ + /** @see AudioFile.genreNames */ var genreNames: List = listOf() ) { - fun copyToRaw(rawSong: RawSong) { - rawSong.musicBrainzId = musicBrainzId - rawSong.name = name - rawSong.sortName = sortName + fun copyToRaw(audioFile: AudioFile) { + audioFile.musicBrainzId = musicBrainzId + audioFile.name = name + audioFile.sortName = sortName - rawSong.durationMs = durationMs + audioFile.durationMs = durationMs - rawSong.replayGainTrackAdjustment = replayGainTrackAdjustment - rawSong.replayGainAlbumAdjustment = replayGainAlbumAdjustment + audioFile.replayGainTrackAdjustment = replayGainTrackAdjustment + audioFile.replayGainAlbumAdjustment = replayGainAlbumAdjustment - rawSong.track = track - rawSong.disc = disc - rawSong.subtitle = subtitle - rawSong.date = date + audioFile.track = track + audioFile.disc = disc + audioFile.subtitle = subtitle + audioFile.date = date - rawSong.albumMusicBrainzId = albumMusicBrainzId - rawSong.albumName = albumName - rawSong.albumSortName = albumSortName - rawSong.releaseTypes = releaseTypes + audioFile.albumMusicBrainzId = albumMusicBrainzId + audioFile.albumName = albumName + audioFile.albumSortName = albumSortName + audioFile.releaseTypes = releaseTypes - rawSong.artistMusicBrainzIds = artistMusicBrainzIds - rawSong.artistNames = artistNames - rawSong.artistSortNames = artistSortNames + audioFile.artistMusicBrainzIds = artistMusicBrainzIds + audioFile.artistNames = artistNames + audioFile.artistSortNames = artistSortNames - rawSong.albumArtistMusicBrainzIds = albumArtistMusicBrainzIds - rawSong.albumArtistNames = albumArtistNames - rawSong.albumArtistSortNames = albumArtistSortNames + audioFile.albumArtistMusicBrainzIds = albumArtistMusicBrainzIds + audioFile.albumArtistNames = albumArtistNames + audioFile.albumArtistSortNames = albumArtistSortNames - rawSong.genreNames = genreNames + audioFile.genreNames = genreNames } object Converters { @@ -144,30 +144,30 @@ data class Tags( } companion object { - fun fromRaw(rawSong: RawSong) = + fun fromRaw(audioFile: AudioFile) = Tags( - uri = rawSong.file.uri.toString(), - dateModified = rawSong.file.lastModified, - musicBrainzId = rawSong.musicBrainzId, - name = requireNotNull(rawSong.name) { "Invalid raw: No name" }, - sortName = rawSong.sortName, - durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" }, - replayGainTrackAdjustment = rawSong.replayGainTrackAdjustment, - replayGainAlbumAdjustment = rawSong.replayGainAlbumAdjustment, - track = rawSong.track, - disc = rawSong.disc, - subtitle = rawSong.subtitle, - date = rawSong.date, - albumMusicBrainzId = rawSong.albumMusicBrainzId, - albumName = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" }, - albumSortName = rawSong.albumSortName, - releaseTypes = rawSong.releaseTypes, - artistMusicBrainzIds = rawSong.artistMusicBrainzIds, - artistNames = rawSong.artistNames, - artistSortNames = rawSong.artistSortNames, - albumArtistMusicBrainzIds = rawSong.albumArtistMusicBrainzIds, - albumArtistNames = rawSong.albumArtistNames, - albumArtistSortNames = rawSong.albumArtistSortNames, - genreNames = rawSong.genreNames) + uri = audioFile.deviceFile.uri.toString(), + dateModified = audioFile.deviceFile.lastModified, + musicBrainzId = audioFile.musicBrainzId, + name = requireNotNull(audioFile.name) { "Invalid raw: No name" }, + sortName = audioFile.sortName, + durationMs = requireNotNull(audioFile.durationMs) { "Invalid raw: No duration" }, + replayGainTrackAdjustment = audioFile.replayGainTrackAdjustment, + replayGainAlbumAdjustment = audioFile.replayGainAlbumAdjustment, + track = audioFile.track, + disc = audioFile.disc, + subtitle = audioFile.subtitle, + date = audioFile.date, + albumMusicBrainzId = audioFile.albumMusicBrainzId, + albumName = requireNotNull(audioFile.albumName) { "Invalid raw: No album name" }, + albumSortName = audioFile.albumSortName, + releaseTypes = audioFile.releaseTypes, + artistMusicBrainzIds = audioFile.artistMusicBrainzIds, + artistNames = audioFile.artistNames, + artistSortNames = audioFile.artistSortNames, + albumArtistMusicBrainzIds = audioFile.albumArtistMusicBrainzIds, + albumArtistNames = audioFile.albumArtistNames, + albumArtistSortNames = audioFile.albumArtistSortNames, + genreNames = audioFile.genreNames) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/ExoPlayerTagExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/ExoPlayerTagExtractor.kt index a45917284..d1cb92c47 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/ExoPlayerTagExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/ExoPlayerTagExtractor.kt @@ -28,12 +28,12 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow -import org.oxycblt.auxio.music.device.RawSong -import org.oxycblt.auxio.music.stack.fs.DeviceFile +import org.oxycblt.auxio.music.stack.AudioFile +import org.oxycblt.auxio.music.stack.DeviceFile import timber.log.Timber as L interface TagResult { - class Hit(val rawSong: RawSong) : TagResult + class Hit(val audioFile: AudioFile) : TagResult class Miss(val file: DeviceFile) : TagResult } @@ -76,9 +76,9 @@ constructor( return } val textTags = TextTags(metadata) - val rawSong = RawSong(file = input) - tagInterpreter2.interpretOn(textTags, rawSong) - collector.emit(TagResult.Hit(rawSong)) + val audioFile = AudioFile(deviceFile = input) + tagInterpreter2.interpretOn(textTags, audioFile) + collector.emit(TagResult.Hit(audioFile)) } private suspend fun noMetadata(input: DeviceFile) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagInterpreter.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagInterpreter.kt index 1619f20b3..7658ee760 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagInterpreter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/extractor/TagInterpreter.kt @@ -21,13 +21,13 @@ package org.oxycblt.auxio.music.stack.extractor import androidx.core.text.isDigitsOnly import androidx.media3.exoplayer.MetadataRetriever import javax.inject.Inject -import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.music.stack.AudioFile import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.util.nonZeroOrNull /** * An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on - * [RawSong] instances. + * [AudioFile] instances. * * @author Alexander Capehart (OxygenCobalt) */ @@ -35,31 +35,31 @@ interface TagInterpreter { /** * Poll to see if this worker is done processing. * - * @return A completed [RawSong] if done, null otherwise. + * @return A completed [AudioFile] if done, null otherwise. */ - fun interpretOn(textTags: TextTags, rawSong: RawSong) + fun interpretOn(textTags: TextTags, audioFile: AudioFile) } class TagInterpreterImpl @Inject constructor() : TagInterpreter { - override fun interpretOn(textTags: TextTags, rawSong: RawSong) { - populateWithId3v2(rawSong, textTags.id3v2) - populateWithVorbis(rawSong, textTags.vorbis) + override fun interpretOn(textTags: TextTags, audioFile: AudioFile) { + populateWithId3v2(audioFile, textTags.id3v2) + populateWithVorbis(audioFile, textTags.vorbis) } - private fun populateWithId3v2(rawSong: RawSong, textFrames: Map>) { + private fun populateWithId3v2(audioFile: AudioFile, textFrames: Map>) { // Song (textFrames["TXXX:musicbrainz release track id"] ?: textFrames["TXXX:musicbrainz_releasetrackid"]) - ?.let { rawSong.musicBrainzId = it.first() } - textFrames["TIT2"]?.let { rawSong.name = it.first() } - textFrames["TSOT"]?.let { rawSong.sortName = it.first() } + ?.let { audioFile.musicBrainzId = it.first() } + textFrames["TIT2"]?.let { audioFile.name = it.first() } + textFrames["TSOT"]?.let { audioFile.sortName = it.first() } // Track. - textFrames["TRCK"]?.run { first().parseId3v2PositionField() }?.let { rawSong.track = it } + textFrames["TRCK"]?.run { first().parseId3v2PositionField() }?.let { audioFile.track = it } // Disc and it's subtitle name. - textFrames["TPOS"]?.run { first().parseId3v2PositionField() }?.let { rawSong.disc = it } - textFrames["TSST"]?.let { rawSong.subtitle = it.first() } + textFrames["TPOS"]?.run { first().parseId3v2PositionField() }?.let { audioFile.disc = it } + textFrames["TSST"]?.let { audioFile.subtitle = it.first() } // Dates are somewhat complicated, as not only did their semantics change from a flat year // value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of @@ -77,27 +77,27 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter { ?: textFrames["TDRC"]?.run { Date.from(first()) } ?: textFrames["TDRL"]?.run { Date.from(first()) } ?: parseId3v23Date(textFrames)) - ?.let { rawSong.date = it } + ?.let { audioFile.date = it } // Album (textFrames["TXXX:musicbrainz album id"] ?: textFrames["TXXX:musicbrainz_albumid"])?.let { - rawSong.albumMusicBrainzId = it.first() + audioFile.albumMusicBrainzId = it.first() } - textFrames["TALB"]?.let { rawSong.albumName = it.first() } - textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() } + textFrames["TALB"]?.let { audioFile.albumName = it.first() } + textFrames["TSOA"]?.let { audioFile.albumSortName = it.first() } (textFrames["TXXX:musicbrainz album type"] ?: textFrames["TXXX:releasetype"] ?: // This is a non-standard iTunes extension textFrames["GRP1"]) - ?.let { rawSong.releaseTypes = it } + ?.let { audioFile.releaseTypes = it } // Artist (textFrames["TXXX:musicbrainz artist id"] ?: textFrames["TXXX:musicbrainz_artistid"])?.let { - rawSong.artistMusicBrainzIds = it + audioFile.artistMusicBrainzIds = it } (textFrames["TXXX:artists"] ?: textFrames["TPE1"] ?: textFrames["TXXX:artist"])?.let { - rawSong.artistNames = it + audioFile.artistNames = it } (textFrames["TXXX:artistssort"] ?: textFrames["TXXX:artists_sort"] @@ -105,19 +105,19 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter { ?: textFrames["TSOP"] ?: textFrames["artistsort"] ?: textFrames["TXXX:artist sort"]) - ?.let { rawSong.artistSortNames = it } + ?.let { audioFile.artistSortNames = it } // Album artist (textFrames["TXXX:musicbrainz album artist id"] ?: textFrames["TXXX:musicbrainz_albumartistid"]) - ?.let { rawSong.albumArtistMusicBrainzIds = it } + ?.let { audioFile.albumArtistMusicBrainzIds = it } (textFrames["TXXX:albumartists"] ?: textFrames["TXXX:album_artists"] ?: textFrames["TXXX:album artists"] ?: textFrames["TPE2"] ?: textFrames["TXXX:albumartist"] ?: textFrames["TXXX:album artist"]) - ?.let { rawSong.albumArtistNames = it } + ?.let { audioFile.albumArtistNames = it } (textFrames["TXXX:albumartistssort"] ?: textFrames["TXXX:albumartists_sort"] ?: textFrames["TXXX:albumartists sort"] @@ -125,10 +125,10 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter { // This is a non-standard iTunes extension ?: textFrames["TSO2"] ?: textFrames["TXXX:album artist sort"]) - ?.let { rawSong.albumArtistSortNames = it } + ?.let { audioFile.albumArtistSortNames = it } // Genre - textFrames["TCON"]?.let { rawSong.genreNames = it } + textFrames["TCON"]?.let { audioFile.genreNames = it } // Compilation Flag (textFrames["TCMP"] // This is a non-standard itunes extension @@ -137,17 +137,17 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter { // Ignore invalid instances of this tag if (it.size != 1 || it[0] != "1") return@let // Change the metadata to be a compilation album made by "Various Artists" - rawSong.albumArtistNames = - rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } - rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } + audioFile.albumArtistNames = + audioFile.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } + audioFile.releaseTypes = audioFile.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } } // ReplayGain information textFrames["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()?.let { - rawSong.replayGainTrackAdjustment = it + audioFile.replayGainTrackAdjustment = it } textFrames["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment()?.let { - rawSong.replayGainAlbumAdjustment = it + audioFile.replayGainAlbumAdjustment = it } } @@ -185,26 +185,26 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter { } } - private fun populateWithVorbis(rawSong: RawSong, comments: Map>) { + private fun populateWithVorbis(audioFile: AudioFile, comments: Map>) { // Song (comments["musicbrainz_releasetrackid"] ?: comments["musicbrainz release track id"])?.let { - rawSong.musicBrainzId = it.first() + audioFile.musicBrainzId = it.first() } - comments["title"]?.let { rawSong.name = it.first() } - comments["titlesort"]?.let { rawSong.sortName = it.first() } + comments["title"]?.let { audioFile.name = it.first() } + comments["titlesort"]?.let { audioFile.sortName = it.first() } // Track. parseVorbisPositionField( comments["tracknumber"]?.first(), (comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first()) - ?.let { rawSong.track = it } + ?.let { audioFile.track = it } // Disc and it's subtitle name. parseVorbisPositionField( comments["discnumber"]?.first(), (comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first()) - ?.let { rawSong.disc = it } - comments["discsubtitle"]?.let { rawSong.subtitle = it.first() } + ?.let { audioFile.disc = it } + comments["discsubtitle"]?.let { audioFile.subtitle = it.first() } // Vorbis dates are less complicated, but there are still several types // Our hierarchy for dates is as such: @@ -215,58 +215,58 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter { (comments["originaldate"]?.run { Date.from(first()) } ?: comments["date"]?.run { Date.from(first()) } ?: comments["year"]?.run { Date.from(first()) }) - ?.let { rawSong.date = it } + ?.let { audioFile.date = it } // Album (comments["musicbrainz_albumid"] ?: comments["musicbrainz album id"])?.let { - rawSong.albumMusicBrainzId = it.first() + audioFile.albumMusicBrainzId = it.first() } - comments["album"]?.let { rawSong.albumName = it.first() } - comments["albumsort"]?.let { rawSong.albumSortName = it.first() } + comments["album"]?.let { audioFile.albumName = it.first() } + comments["albumsort"]?.let { audioFile.albumSortName = it.first() } (comments["releasetype"] ?: comments["musicbrainz album type"])?.let { - rawSong.releaseTypes = it + audioFile.releaseTypes = it } // Artist (comments["musicbrainz_artistid"] ?: comments["musicbrainz artist id"])?.let { - rawSong.artistMusicBrainzIds = it + audioFile.artistMusicBrainzIds = it } - (comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it } + (comments["artists"] ?: comments["artist"])?.let { audioFile.artistNames = it } (comments["artistssort"] ?: comments["artists_sort"] ?: comments["artists sort"] ?: comments["artistsort"] ?: comments["artist sort"]) - ?.let { rawSong.artistSortNames = it } + ?.let { audioFile.artistSortNames = it } // Album artist (comments["musicbrainz_albumartistid"] ?: comments["musicbrainz album artist id"])?.let { - rawSong.albumArtistMusicBrainzIds = it + audioFile.albumArtistMusicBrainzIds = it } (comments["albumartists"] ?: comments["album_artists"] ?: comments["album artists"] ?: comments["albumartist"] ?: comments["album artist"]) - ?.let { rawSong.albumArtistNames = it } + ?.let { audioFile.albumArtistNames = it } (comments["albumartistssort"] ?: comments["albumartists_sort"] ?: comments["albumartists sort"] ?: comments["albumartistsort"] ?: comments["album artist sort"]) - ?.let { rawSong.albumArtistSortNames = it } + ?.let { audioFile.albumArtistSortNames = it } // Genre - comments["genre"]?.let { rawSong.genreNames = it } + comments["genre"]?.let { audioFile.genreNames = it } // Compilation Flag (comments["compilation"] ?: comments["itunescompilation"])?.let { // Ignore invalid instances of this tag if (it.size != 1 || it[0] != "1") return@let // Change the metadata to be a compilation album made by "Various Artists" - rawSong.albumArtistNames = - rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } - rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } + audioFile.albumArtistNames = + audioFile.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS } + audioFile.releaseTypes = audioFile.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES } } // ReplayGain information @@ -278,10 +278,10 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter { // tags anyway. (comments["r128_track_gain"]?.parseR128Adjustment() ?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment()) - ?.let { rawSong.replayGainTrackAdjustment = it } + ?.let { audioFile.replayGainTrackAdjustment = it } (comments["r128_album_gain"]?.parseR128Adjustment() ?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment()) - ?.let { rawSong.replayGainAlbumAdjustment = it } + ?.let { audioFile.replayGainAlbumAdjustment = it } } private fun List.parseR128Adjustment() = diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt index 9ac4e65f5..d5cabaa1e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/fs/DeviceFiles.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flow +import org.oxycblt.auxio.music.stack.DeviceFile interface DeviceFiles { fun explore(uris: Flow): Flow @@ -107,10 +108,3 @@ constructor( } } -data class DeviceFile( - val uri: Uri, - val mimeType: String, - val path: Path, - val size: Long, - val lastModified: Long -) diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt index 2e12f1bff..9f739a27e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/PlaylistImpl.kt @@ -23,7 +23,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.device.DeviceLibrary +import org.oxycblt.auxio.music.model.DeviceLibrary import org.oxycblt.auxio.music.info.Name class PlaylistImpl diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index c2dc32b5f..f166b630d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -24,7 +24,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.device.DeviceLibrary +import org.oxycblt.auxio.music.model.DeviceLibrary import org.oxycblt.auxio.music.info.Name import timber.log.Timber as L diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt index 62c1ad914..c3b85b9a1 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionInterface.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.device.DeviceLibrary +import org.oxycblt.auxio.music.model.DeviceLibrary import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.service.MediaSessionUID import org.oxycblt.auxio.music.service.MusicBrowser diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 4f2c6070b..6a0ab9240 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -36,7 +36,7 @@ import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.device.DeviceLibrary +import org.oxycblt.auxio.music.model.DeviceLibrary import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings diff --git a/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt index 23a791662..9f3545214 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/cache/CacheRepositoryTest.kt @@ -31,7 +31,7 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.music.stack.AudioFile import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.stack.cache.TagDao import org.oxycblt.auxio.music.stack.cache.Tags @@ -48,13 +48,13 @@ class CacheRepositoryTest { coVerifyAll { dao.readSongs() } assertFalse(cache.invalidated) - val songA = RawSong(mediaStoreId = 0, dateAdded = 1, dateModified = 2) + val songA = AudioFile(mediaStoreId = 0, dateAdded = 1, dateModified = 2) assertTrue(cache.populate(songA)) assertEquals(RAW_SONG_A, songA) assertFalse(cache.invalidated) - val songB = RawSong(mediaStoreId = 9, dateAdded = 10, dateModified = 11) + val songB = AudioFile(mediaStoreId = 9, dateAdded = 10, dateModified = 11) assertTrue(cache.populate(songB)) assertEquals(RAW_SONG_B, songB) @@ -72,14 +72,14 @@ class CacheRepositoryTest { coVerifyAll { dao.readSongs() } assertFalse(cache.invalidated) - val nullStart = RawSong(mediaStoreId = 0, dateAdded = 0, dateModified = 0) - val nullEnd = RawSong(mediaStoreId = 0, dateAdded = 0, dateModified = 0) + val nullStart = AudioFile(mediaStoreId = 0, dateAdded = 0, dateModified = 0) + val nullEnd = AudioFile(mediaStoreId = 0, dateAdded = 0, dateModified = 0) assertFalse(cache.populate(nullStart)) assertEquals(nullStart, nullEnd) assertTrue(cache.invalidated) - val songB = RawSong(mediaStoreId = 9, dateAdded = 10, dateModified = 11) + val songB = AudioFile(mediaStoreId = 9, dateAdded = 10, dateModified = 11) assertTrue(cache.populate(songB)) assertEquals(RAW_SONG_B, songB) @@ -179,7 +179,7 @@ class CacheRepositoryTest { ) val RAW_SONG_A = - RawSong( + AudioFile( mediaStoreId = 0, dateAdded = 1, dateModified = 2, @@ -237,7 +237,7 @@ class CacheRepositoryTest { ) val RAW_SONG_B = - RawSong( + AudioFile( mediaStoreId = 9, dateAdded = 10, dateModified = 11, diff --git a/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt b/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt index 9e3649022..30dbad659 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/user/DeviceLibraryTest.kt @@ -26,11 +26,11 @@ import org.junit.Assert.assertNotEquals import org.junit.Test import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicType -import org.oxycblt.auxio.music.device.AlbumImpl -import org.oxycblt.auxio.music.device.ArtistImpl -import org.oxycblt.auxio.music.device.DeviceLibraryImpl -import org.oxycblt.auxio.music.device.GenreImpl -import org.oxycblt.auxio.music.device.SongImpl +import org.oxycblt.auxio.music.model.AlbumImpl +import org.oxycblt.auxio.music.model.ArtistImpl +import org.oxycblt.auxio.music.model.DeviceLibraryImpl +import org.oxycblt.auxio.music.model.GenreImpl +import org.oxycblt.auxio.music.model.SongImpl import org.oxycblt.auxio.music.stack.fs.Components import org.oxycblt.auxio.music.stack.fs.Path