Introduce Interpreter
This is utterly broken and mostly a starting point for future refactoring.
This commit is contained in:
parent
c022be6e4d
commit
517da485e1
27 changed files with 921 additions and 1318 deletions
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Song>
|
||||
|
||||
/** All [Album]s in this [DeviceLibrary]. */
|
||||
val albums: Collection<Album>
|
||||
|
||||
/** All [Artist]s in this [DeviceLibrary]. */
|
||||
val artists: Collection<Artist>
|
||||
|
||||
/** All [Genre]s in this [DeviceLibrary]. */
|
||||
val genres: Collection<Genre>
|
||||
|
||||
/**
|
||||
* 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<RawSong>,
|
||||
onSongProcessed: () -> Unit,
|
||||
separators: Separators,
|
||||
nameFactory: Name.Known.Factory
|
||||
): DeviceLibraryImpl
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory {
|
||||
override suspend fun create(
|
||||
rawSongs: Flow<RawSong>,
|
||||
onSongProcessed: () -> Unit,
|
||||
separators: Separators,
|
||||
nameFactory: Name.Known.Factory
|
||||
): DeviceLibraryImpl {
|
||||
val songGrouping = mutableMapOf<Music.UID, SongImpl>()
|
||||
val albumGrouping = mutableMapOf<String?, MutableMap<UUID?, Grouping<RawAlbum, SongImpl>>>()
|
||||
val artistGrouping = mutableMapOf<String?, MutableMap<UUID?, Grouping<RawArtist, Music>>>()
|
||||
val genreGrouping = mutableMapOf<String?, Grouping<RawGenre, SongImpl>>()
|
||||
|
||||
// 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 <R : NameGroupable, O : Music, N : O> appendToNameTree(
|
||||
music: N,
|
||||
raw: R,
|
||||
tree: MutableMap<String?, Grouping<R, O>>,
|
||||
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 <R : NameGroupable, O : Music, P : MusicParent> flattenNameTree(
|
||||
tree: MutableMap<String?, Grouping<R, O>>,
|
||||
map: (Grouping<R, O>) -> P
|
||||
): Set<P> = tree.values.mapTo(mutableSetOf()) { map(it) }
|
||||
|
||||
private inline fun <R : MusicBrainzGroupable, O : Music, N : O> appendToMusicBrainzIdTree(
|
||||
music: N,
|
||||
raw: R,
|
||||
tree: MutableMap<String?, MutableMap<UUID?, Grouping<R, O>>>,
|
||||
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 <R, M : Music> pruneMusicBrainzIdTree(
|
||||
tree: MutableMap<String?, MutableMap<UUID?, Grouping<R, M>>>,
|
||||
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 <R, M : Music, T : MusicParent> flattenMusicBrainzIdTree(
|
||||
tree: MutableMap<String?, MutableMap<UUID?, Grouping<R, M>>>,
|
||||
map: (Grouping<R, M>) -> T
|
||||
): Set<T> {
|
||||
val result = mutableSetOf<T>()
|
||||
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<SongImpl>,
|
||||
override val albums: Collection<AlbumImpl>,
|
||||
override val artists: Collection<ArtistImpl>,
|
||||
override val genres: Collection<GenreImpl>
|
||||
) : 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 }
|
||||
// }
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<ArtistImpl>()
|
||||
override val artists: List<Artist>
|
||||
get() = _artists
|
||||
|
||||
private val _genres = mutableListOf<GenreImpl>()
|
||||
override val genres: List<Genre>
|
||||
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<RawArtist>
|
||||
|
||||
/**
|
||||
* 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<RawGenre>
|
||||
|
||||
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<RawAlbum, SongImpl>,
|
||||
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<ArtistImpl>()
|
||||
override val artists: List<Artist>
|
||||
get() = _artists
|
||||
|
||||
override val songs: Set<Song> = 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<RawArtist, Music>,
|
||||
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<Song>
|
||||
override val explicitAlbums: Set<Album>
|
||||
override val implicitAlbums: Set<Album>
|
||||
override val durationMs: Long?
|
||||
override val cover: ParentCover
|
||||
|
||||
override lateinit var genres: List<Genre>
|
||||
|
||||
private var hashCode = uid.hashCode()
|
||||
|
||||
init {
|
||||
val distinctSongs = mutableSetOf<Song>()
|
||||
val albumMap = mutableMapOf<Album, Boolean>()
|
||||
|
||||
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<RawArtist>) =
|
||||
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<RawGenre, SongImpl>,
|
||||
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<Song>
|
||||
override val artists: Set<Artist>
|
||||
override val durationMs: Long
|
||||
override val cover: ParentCover
|
||||
|
||||
private var hashCode = uid.hashCode()
|
||||
|
||||
init {
|
||||
val distinctArtists = mutableSetOf<Artist>()
|
||||
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<RawGenre>) =
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String> = listOf(),
|
||||
/** @see RawArtist.musicBrainzId */
|
||||
var artistMusicBrainzIds: List<String> = listOf(),
|
||||
/** @see RawArtist.name */
|
||||
var artistNames: List<String> = listOf(),
|
||||
/** @see RawArtist.sortName */
|
||||
var artistSortNames: List<String> = listOf(),
|
||||
/** @see RawArtist.musicBrainzId */
|
||||
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
||||
/** @see RawArtist.name */
|
||||
var albumArtistNames: List<String> = listOf(),
|
||||
/** @see RawArtist.sortName */
|
||||
var albumArtistSortNames: List<String> = listOf(),
|
||||
/** @see RawGenre.name */
|
||||
var genreNames: List<String> = 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<RawArtist>
|
||||
) : 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<R, M : Music>(var raw: PrioritizedRaw<R, M>, val music: MutableSet<M>)
|
||||
|
||||
/**
|
||||
* 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<R, M : Music>(val inner: R, val src: M)
|
|
@ -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
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Song>()
|
||||
|
||||
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<Song>()
|
||||
|
||||
private val albums = mutableSetOf<Album>()
|
||||
private val albumMap = mutableMapOf<Album, Boolean>()
|
||||
override lateinit var explicitAlbums: Set<Album>
|
||||
override lateinit var implicitAlbums: Set<Album>
|
||||
|
||||
|
||||
override lateinit var genres: List<Genre>
|
||||
|
||||
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<Song>()
|
||||
override val artists = mutableSetOf<Artist>()
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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<AudioFile>,
|
||||
playlistFiles: Flow<PlaylistFile>,
|
||||
interpretation: Interpretation
|
||||
): MutableLibrary
|
||||
}
|
||||
|
||||
class LinkedSong(private val albumLinkedSong: AlbumInterpreter.LinkedSong) {
|
||||
val preSong: PreSong get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.preSong
|
||||
val album: Linked<AlbumImpl, SongImpl> get() = albumLinkedSong.album
|
||||
val artists: Linked<List<ArtistImpl>, SongImpl> get() = albumLinkedSong.linkedArtistSong.artists
|
||||
val genres: Linked<List<GenreImpl>, SongImpl> get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.genres
|
||||
}
|
||||
|
||||
typealias LinkedAlbum = ArtistInterpreter.LinkedAlbum
|
||||
|
||||
class InterpreterImpl(
|
||||
private val songInterpreter: SongInterpreter
|
||||
) : Interpreter {
|
||||
override suspend fun interpret(
|
||||
audioFiles: Flow<AudioFile>,
|
||||
playlistFiles: Flow<PlaylistFile>,
|
||||
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 {
|
||||
}
|
||||
|
||||
}
|
79
app/src/main/java/org/oxycblt/auxio/music/model/Library.kt
Normal file
79
app/src/main/java/org/oxycblt/auxio/music/model/Library.kt
Normal file
|
@ -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<Song>
|
||||
val albums: Collection<Album>
|
||||
val artists: Collection<Artist>
|
||||
val genres: Collection<Genre>
|
||||
val playlists: Collection<Playlist>
|
||||
|
||||
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<Song>): MutableLibrary
|
||||
suspend fun renamePlaylist(playlist: Playlist, name: String): MutableLibrary
|
||||
suspend fun addToPlaylist(playlist: Playlist, songs: List<Song>): MutableLibrary
|
||||
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): MutableLibrary
|
||||
suspend fun deletePlaylist(playlist: Playlist): MutableLibrary
|
||||
}
|
||||
|
||||
class LibraryImpl(
|
||||
override val songs: Collection<SongImpl>,
|
||||
override val albums: Collection<AlbumImpl>,
|
||||
override val artists: Collection<ArtistImpl>,
|
||||
override val genres: Collection<GenreImpl>
|
||||
) : MutableLibrary {
|
||||
override val playlists = emptySet<Playlist>()
|
||||
|
||||
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<Song>): 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<Song>): MutableLibrary {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): MutableLibrary {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun deletePlaylist(playlist: Playlist): MutableLibrary {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
53
app/src/main/java/org/oxycblt/auxio/music/model/PreMusic.kt
Normal file
53
app/src/main/java/org/oxycblt/auxio/music/model/PreMusic.kt
Normal file
|
@ -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<PreArtist>,
|
||||
val preGenres: List<PreGenre>
|
||||
)
|
||||
|
||||
data class PreAlbum(
|
||||
val musicBrainzId: UUID?,
|
||||
val name: Name,
|
||||
val rawName: String,
|
||||
val releaseType: ReleaseType,
|
||||
val preArtists: List<PreArtist>
|
||||
)
|
||||
|
||||
data class PreArtist(
|
||||
val musicBrainzId: UUID?,
|
||||
val name: Name,
|
||||
val rawName: String?,
|
||||
)
|
||||
|
||||
data class PreGenre(
|
||||
val name: Name,
|
||||
val rawName: String?,
|
||||
)
|
|
@ -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<AudioFile>, interpretation: Interpretation): Flow<PreSong>
|
||||
}
|
||||
|
||||
class SongInterpreterImpl(
|
||||
private val nameFactory: Name.Known.Factory,
|
||||
private val separators: Separators
|
||||
) : SongInterpreter {
|
||||
override fun prepare(audioFiles: Flow<AudioFile>, 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 <T> 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<PreArtist>,
|
||||
albumPreArtists: List<PreArtist>
|
||||
): 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<String>,
|
||||
rawNames: List<String>,
|
||||
rawSortNames: List<String>
|
||||
): List<PreArtist> {
|
||||
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<PreGenre> {
|
||||
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)
|
||||
|
||||
}
|
44
app/src/main/java/org/oxycblt/auxio/music/model/Trees.kt
Normal file
44
app/src/main/java/org/oxycblt/auxio/music/model/Trees.kt
Normal file
|
@ -0,0 +1,44 @@
|
|||
package org.oxycblt.auxio.music.model
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
interface AlbumInterpreter {
|
||||
suspend fun register(linkedSongs: Flow<ArtistInterpreter.LinkedSong>): Flow<LinkedSong>
|
||||
fun resolve(): Collection<AlbumImpl>
|
||||
|
||||
data class LinkedSong(
|
||||
val linkedArtistSong: ArtistInterpreter.LinkedSong,
|
||||
val album: Linked<AlbumImpl, SongImpl>
|
||||
)
|
||||
}
|
||||
|
||||
interface ArtistInterpreter {
|
||||
suspend fun register(preSong: Flow<GenreInterpreter.LinkedSong>): Flow<LinkedSong>
|
||||
fun resolve(): Collection<ArtistImpl>
|
||||
|
||||
data class LinkedSong(
|
||||
val linkedGenreSong: GenreInterpreter.LinkedSong,
|
||||
val linkedAlbum: LinkedAlbum,
|
||||
val artists: Linked<List<ArtistImpl>, SongImpl>
|
||||
)
|
||||
|
||||
data class LinkedAlbum(
|
||||
val preAlbum: PreAlbum,
|
||||
val artists: Linked<List<ArtistImpl>, AlbumImpl>
|
||||
)
|
||||
}
|
||||
|
||||
interface GenreInterpreter {
|
||||
suspend fun register(preSong: Flow<PreSong>): Flow<LinkedSong>
|
||||
fun resolve(): Collection<GenreImpl>
|
||||
|
||||
data class LinkedSong(
|
||||
val preSong: PreSong,
|
||||
val genres: Linked<List<GenreImpl>, SongImpl>
|
||||
)
|
||||
}
|
||||
|
||||
interface Linked<P, C> {
|
||||
fun resolve(child: C): P
|
||||
}
|
66
app/src/main/java/org/oxycblt/auxio/music/stack/Files.kt
Normal file
66
app/src/main/java/org/oxycblt/auxio/music/stack/Files.kt
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String> = listOf(),
|
||||
var artistMusicBrainzIds: List<String> = listOf(),
|
||||
var artistNames: List<String> = listOf(),
|
||||
var artistSortNames: List<String> = listOf(),
|
||||
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
||||
var albumArtistNames: List<String> = listOf(),
|
||||
var albumArtistSortNames: List<String> = listOf(),
|
||||
var genreNames: List<String> = listOf()
|
||||
)
|
||||
|
||||
interface PlaylistFile {
|
||||
val name: String
|
||||
}
|
|
@ -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<Uri>,
|
||||
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<Uri>,
|
||||
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<TagResult>.split(): Pair<Flow<DeviceFile>, Flow<RawSong>> {
|
||||
private fun Flow<TagResult>.results(): Pair<Flow<DeviceFile>, Flow<AudioFile>> {
|
||||
val files = filterIsInstance<TagResult.Miss>().map { it.file }
|
||||
val songs = filterIsInstance<TagResult.Hit>().map { it.rawSong }
|
||||
val songs = filterIsInstance<TagResult.Hit>().map { it.audioFile }
|
||||
return files to songs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<DeviceFile>): Flow<TagResult>
|
||||
|
||||
suspend fun write(rawSongs: Flow<RawSong>)
|
||||
suspend fun write(rawSongs: Flow<AudioFile>)
|
||||
}
|
||||
|
||||
class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache {
|
||||
|
@ -36,15 +36,15 @@ class TagCacheImpl @Inject constructor(private val tagDao: TagDao) : TagCache {
|
|||
files.transform<DeviceFile, TagResult> { 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<RawSong>) {
|
||||
override suspend fun write(rawSongs: Flow<AudioFile>) {
|
||||
rawSongs.collect { rawSong -> tagDao.updateTags(Tags.fromRaw(rawSong)) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> = listOf(),
|
||||
/** @see RawSong.artistMusicBrainzIds */
|
||||
/** @see AudioFile.artistMusicBrainzIds */
|
||||
var artistMusicBrainzIds: List<String> = listOf(),
|
||||
/** @see RawSong.artistNames */
|
||||
/** @see AudioFile.artistNames */
|
||||
var artistNames: List<String> = listOf(),
|
||||
/** @see RawSong.artistSortNames */
|
||||
/** @see AudioFile.artistSortNames */
|
||||
var artistSortNames: List<String> = listOf(),
|
||||
/** @see RawSong.albumArtistMusicBrainzIds */
|
||||
/** @see AudioFile.albumArtistMusicBrainzIds */
|
||||
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
||||
/** @see RawSong.albumArtistNames */
|
||||
/** @see AudioFile.albumArtistNames */
|
||||
var albumArtistNames: List<String> = listOf(),
|
||||
/** @see RawSong.albumArtistSortNames */
|
||||
/** @see AudioFile.albumArtistSortNames */
|
||||
var albumArtistSortNames: List<String> = listOf(),
|
||||
/** @see RawSong.genreNames */
|
||||
/** @see AudioFile.genreNames */
|
||||
var genreNames: List<String> = 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<String, List<String>>) {
|
||||
private fun populateWithId3v2(audioFile: AudioFile, textFrames: Map<String, List<String>>) {
|
||||
// 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<String, List<String>>) {
|
||||
private fun populateWithVorbis(audioFile: AudioFile, comments: Map<String, List<String>>) {
|
||||
// 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<String>.parseR128Adjustment() =
|
||||
|
|
|
@ -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<Uri>): Flow<DeviceFile>
|
||||
|
@ -107,10 +108,3 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
data class DeviceFile(
|
||||
val uri: Uri,
|
||||
val mimeType: String,
|
||||
val path: Path,
|
||||
val size: Long,
|
||||
val lastModified: Long
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue