) -> 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