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.Music
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.Song
|
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
|
import timber.log.Timber as L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -29,7 +29,7 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.yield
|
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.info.Name
|
||||||
import org.oxycblt.auxio.music.metadata.Separators
|
import org.oxycblt.auxio.music.metadata.Separators
|
||||||
import org.oxycblt.auxio.music.stack.Indexer
|
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.
|
* Name.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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/>.
|
* 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.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
@ -25,6 +25,8 @@ import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface DeviceModule {
|
interface ModelModule {
|
||||||
@Binds fun deviceLibraryFactory(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory
|
@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.SharingStarted
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.buffer
|
import kotlinx.coroutines.flow.buffer
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
import kotlinx.coroutines.flow.shareIn
|
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.info.Name
|
||||||
import org.oxycblt.auxio.music.metadata.Separators
|
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.cache.TagCache
|
||||||
import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor
|
import org.oxycblt.auxio.music.stack.extractor.ExoPlayerTagExtractor
|
||||||
import org.oxycblt.auxio.music.stack.extractor.TagResult
|
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.stack.fs.DeviceFiles
|
||||||
import org.oxycblt.auxio.music.user.MutableUserLibrary
|
|
||||||
import org.oxycblt.auxio.music.user.UserLibrary
|
|
||||||
|
|
||||||
interface Indexer {
|
interface Indexer {
|
||||||
suspend fun run(
|
suspend fun run(
|
||||||
uris: List<Uri>,
|
uris: List<Uri>,
|
||||||
separators: Separators,
|
interpretation: Interpretation
|
||||||
nameFactory: Name.Known.Factory
|
): MutableLibrary
|
||||||
): LibraryResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class LibraryResult(val deviceLibrary: DeviceLibrary, val userLibrary: MutableUserLibrary)
|
|
||||||
|
|
||||||
class IndexerImpl
|
class IndexerImpl
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -61,39 +58,34 @@ constructor(
|
||||||
private val deviceFiles: DeviceFiles,
|
private val deviceFiles: DeviceFiles,
|
||||||
private val tagCache: TagCache,
|
private val tagCache: TagCache,
|
||||||
private val tagExtractor: ExoPlayerTagExtractor,
|
private val tagExtractor: ExoPlayerTagExtractor,
|
||||||
private val deviceLibraryFactory: DeviceLibrary.Factory,
|
private val interpreter: Interpreter
|
||||||
private val userLibraryFactory: UserLibrary.Factory
|
|
||||||
) : Indexer {
|
) : Indexer {
|
||||||
override suspend fun run(
|
override suspend fun run(
|
||||||
uris: List<Uri>,
|
uris: List<Uri>,
|
||||||
separators: Separators,
|
interpretation: Interpretation
|
||||||
nameFactory: Name.Known.Factory
|
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer()
|
val deviceFiles = deviceFiles.explore(uris.asFlow()).flowOn(Dispatchers.IO).buffer()
|
||||||
val tagRead = tagCache.read(deviceFiles).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 tagExtractor = tagExtractor.process(cacheFiles).flowOn(Dispatchers.IO).buffer()
|
||||||
val (_, extractorSongs) = tagExtractor.split()
|
val (_, extractorSongs) = tagExtractor.results()
|
||||||
val sharedExtractorSongs =
|
val sharedExtractorSongs =
|
||||||
extractorSongs.shareIn(
|
extractorSongs.shareIn(
|
||||||
CoroutineScope(Dispatchers.Main),
|
CoroutineScope(Dispatchers.Main),
|
||||||
started = SharingStarted.WhileSubscribed(),
|
started = SharingStarted.WhileSubscribed(),
|
||||||
replay = Int.MAX_VALUE)
|
replay = Int.MAX_VALUE)
|
||||||
val tagWrite =
|
val tagWrite =
|
||||||
async(Dispatchers.IO) { tagCache.write(merge(cacheSongs, sharedExtractorSongs)) }
|
async(Dispatchers.IO) { tagCache.write(sharedExtractorSongs) }
|
||||||
val rawPlaylists = async(Dispatchers.IO) { userLibraryFactory.query() }
|
val library = async(Dispatchers.Main) { interpreter.interpret(
|
||||||
val deviceLibrary =
|
merge(cacheSongs, sharedExtractorSongs), emptyFlow(), interpretation
|
||||||
deviceLibraryFactory.create(
|
)}
|
||||||
merge(cacheSongs, sharedExtractorSongs), {}, separators, nameFactory)
|
|
||||||
val userLibrary =
|
|
||||||
userLibraryFactory.create(rawPlaylists.await(), deviceLibrary, nameFactory)
|
|
||||||
tagWrite.await()
|
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 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
|
return files to songs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,14 +21,14 @@ package org.oxycblt.auxio.music.stack.cache
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.transform
|
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.extractor.TagResult
|
||||||
import org.oxycblt.auxio.music.stack.fs.DeviceFile
|
import org.oxycblt.auxio.music.stack.DeviceFile
|
||||||
|
|
||||||
interface TagCache {
|
interface TagCache {
|
||||||
fun read(files: Flow<DeviceFile>): Flow<TagResult>
|
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 {
|
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 ->
|
files.transform<DeviceFile, TagResult> { file ->
|
||||||
val tags = tagDao.selectTags(file.uri.toString(), file.lastModified)
|
val tags = tagDao.selectTags(file.uri.toString(), file.lastModified)
|
||||||
if (tags != null) {
|
if (tags != null) {
|
||||||
val rawSong = RawSong(file = file)
|
val audioFile = AudioFile(deviceFile = file)
|
||||||
tags.copyToRaw(rawSong)
|
tags.copyToRaw(audioFile)
|
||||||
TagResult.Hit(rawSong)
|
TagResult.Hit(audioFile)
|
||||||
} else {
|
} else {
|
||||||
TagResult.Miss(file)
|
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)) }
|
rawSongs.collect { rawSong -> tagDao.updateTags(Tags.fromRaw(rawSong)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import androidx.room.Query
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
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.info.Date
|
||||||
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
import org.oxycblt.auxio.music.stack.extractor.correctWhitespace
|
||||||
import org.oxycblt.auxio.music.stack.extractor.splitEscaped
|
import org.oxycblt.auxio.music.stack.extractor.splitEscaped
|
||||||
|
@ -50,84 +50,84 @@ interface TagDao {
|
||||||
@TypeConverters(Tags.Converters::class)
|
@TypeConverters(Tags.Converters::class)
|
||||||
data class Tags(
|
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.
|
* only used for comparison.
|
||||||
*/
|
*/
|
||||||
@PrimaryKey val uri: String,
|
@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,
|
val dateModified: Long,
|
||||||
/** @see RawSong */
|
/** @see AudioFile */
|
||||||
val durationMs: Long,
|
val durationMs: Long,
|
||||||
/** @see RawSong.replayGainTrackAdjustment */
|
/** @see AudioFile.replayGainTrackAdjustment */
|
||||||
val replayGainTrackAdjustment: Float? = null,
|
val replayGainTrackAdjustment: Float? = null,
|
||||||
/** @see RawSong.replayGainAlbumAdjustment */
|
/** @see AudioFile.replayGainAlbumAdjustment */
|
||||||
val replayGainAlbumAdjustment: Float? = null,
|
val replayGainAlbumAdjustment: Float? = null,
|
||||||
/** @see RawSong.musicBrainzId */
|
/** @see AudioFile.musicBrainzId */
|
||||||
var musicBrainzId: String? = null,
|
var musicBrainzId: String? = null,
|
||||||
/** @see RawSong.name */
|
/** @see AudioFile.name */
|
||||||
var name: String,
|
var name: String,
|
||||||
/** @see RawSong.sortName */
|
/** @see AudioFile.sortName */
|
||||||
var sortName: String? = null,
|
var sortName: String? = null,
|
||||||
/** @see RawSong.track */
|
/** @see AudioFile.track */
|
||||||
var track: Int? = null,
|
var track: Int? = null,
|
||||||
/** @see RawSong.name */
|
/** @see AudioFile.name */
|
||||||
var disc: Int? = null,
|
var disc: Int? = null,
|
||||||
/** @See RawSong.subtitle */
|
/** @See AudioFile.subtitle */
|
||||||
var subtitle: String? = null,
|
var subtitle: String? = null,
|
||||||
/** @see RawSong.date */
|
/** @see AudioFile.date */
|
||||||
var date: Date? = null,
|
var date: Date? = null,
|
||||||
/** @see RawSong.albumMusicBrainzId */
|
/** @see AudioFile.albumMusicBrainzId */
|
||||||
var albumMusicBrainzId: String? = null,
|
var albumMusicBrainzId: String? = null,
|
||||||
/** @see RawSong.albumName */
|
/** @see AudioFile.albumName */
|
||||||
var albumName: String,
|
var albumName: String,
|
||||||
/** @see RawSong.albumSortName */
|
/** @see AudioFile.albumSortName */
|
||||||
var albumSortName: String? = null,
|
var albumSortName: String? = null,
|
||||||
/** @see RawSong.releaseTypes */
|
/** @see AudioFile.releaseTypes */
|
||||||
var releaseTypes: List<String> = listOf(),
|
var releaseTypes: List<String> = listOf(),
|
||||||
/** @see RawSong.artistMusicBrainzIds */
|
/** @see AudioFile.artistMusicBrainzIds */
|
||||||
var artistMusicBrainzIds: List<String> = listOf(),
|
var artistMusicBrainzIds: List<String> = listOf(),
|
||||||
/** @see RawSong.artistNames */
|
/** @see AudioFile.artistNames */
|
||||||
var artistNames: List<String> = listOf(),
|
var artistNames: List<String> = listOf(),
|
||||||
/** @see RawSong.artistSortNames */
|
/** @see AudioFile.artistSortNames */
|
||||||
var artistSortNames: List<String> = listOf(),
|
var artistSortNames: List<String> = listOf(),
|
||||||
/** @see RawSong.albumArtistMusicBrainzIds */
|
/** @see AudioFile.albumArtistMusicBrainzIds */
|
||||||
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
||||||
/** @see RawSong.albumArtistNames */
|
/** @see AudioFile.albumArtistNames */
|
||||||
var albumArtistNames: List<String> = listOf(),
|
var albumArtistNames: List<String> = listOf(),
|
||||||
/** @see RawSong.albumArtistSortNames */
|
/** @see AudioFile.albumArtistSortNames */
|
||||||
var albumArtistSortNames: List<String> = listOf(),
|
var albumArtistSortNames: List<String> = listOf(),
|
||||||
/** @see RawSong.genreNames */
|
/** @see AudioFile.genreNames */
|
||||||
var genreNames: List<String> = listOf()
|
var genreNames: List<String> = listOf()
|
||||||
) {
|
) {
|
||||||
fun copyToRaw(rawSong: RawSong) {
|
fun copyToRaw(audioFile: AudioFile) {
|
||||||
rawSong.musicBrainzId = musicBrainzId
|
audioFile.musicBrainzId = musicBrainzId
|
||||||
rawSong.name = name
|
audioFile.name = name
|
||||||
rawSong.sortName = sortName
|
audioFile.sortName = sortName
|
||||||
|
|
||||||
rawSong.durationMs = durationMs
|
audioFile.durationMs = durationMs
|
||||||
|
|
||||||
rawSong.replayGainTrackAdjustment = replayGainTrackAdjustment
|
audioFile.replayGainTrackAdjustment = replayGainTrackAdjustment
|
||||||
rawSong.replayGainAlbumAdjustment = replayGainAlbumAdjustment
|
audioFile.replayGainAlbumAdjustment = replayGainAlbumAdjustment
|
||||||
|
|
||||||
rawSong.track = track
|
audioFile.track = track
|
||||||
rawSong.disc = disc
|
audioFile.disc = disc
|
||||||
rawSong.subtitle = subtitle
|
audioFile.subtitle = subtitle
|
||||||
rawSong.date = date
|
audioFile.date = date
|
||||||
|
|
||||||
rawSong.albumMusicBrainzId = albumMusicBrainzId
|
audioFile.albumMusicBrainzId = albumMusicBrainzId
|
||||||
rawSong.albumName = albumName
|
audioFile.albumName = albumName
|
||||||
rawSong.albumSortName = albumSortName
|
audioFile.albumSortName = albumSortName
|
||||||
rawSong.releaseTypes = releaseTypes
|
audioFile.releaseTypes = releaseTypes
|
||||||
|
|
||||||
rawSong.artistMusicBrainzIds = artistMusicBrainzIds
|
audioFile.artistMusicBrainzIds = artistMusicBrainzIds
|
||||||
rawSong.artistNames = artistNames
|
audioFile.artistNames = artistNames
|
||||||
rawSong.artistSortNames = artistSortNames
|
audioFile.artistSortNames = artistSortNames
|
||||||
|
|
||||||
rawSong.albumArtistMusicBrainzIds = albumArtistMusicBrainzIds
|
audioFile.albumArtistMusicBrainzIds = albumArtistMusicBrainzIds
|
||||||
rawSong.albumArtistNames = albumArtistNames
|
audioFile.albumArtistNames = albumArtistNames
|
||||||
rawSong.albumArtistSortNames = albumArtistSortNames
|
audioFile.albumArtistSortNames = albumArtistSortNames
|
||||||
|
|
||||||
rawSong.genreNames = genreNames
|
audioFile.genreNames = genreNames
|
||||||
}
|
}
|
||||||
|
|
||||||
object Converters {
|
object Converters {
|
||||||
|
@ -144,30 +144,30 @@ data class Tags(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromRaw(rawSong: RawSong) =
|
fun fromRaw(audioFile: AudioFile) =
|
||||||
Tags(
|
Tags(
|
||||||
uri = rawSong.file.uri.toString(),
|
uri = audioFile.deviceFile.uri.toString(),
|
||||||
dateModified = rawSong.file.lastModified,
|
dateModified = audioFile.deviceFile.lastModified,
|
||||||
musicBrainzId = rawSong.musicBrainzId,
|
musicBrainzId = audioFile.musicBrainzId,
|
||||||
name = requireNotNull(rawSong.name) { "Invalid raw: No name" },
|
name = requireNotNull(audioFile.name) { "Invalid raw: No name" },
|
||||||
sortName = rawSong.sortName,
|
sortName = audioFile.sortName,
|
||||||
durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" },
|
durationMs = requireNotNull(audioFile.durationMs) { "Invalid raw: No duration" },
|
||||||
replayGainTrackAdjustment = rawSong.replayGainTrackAdjustment,
|
replayGainTrackAdjustment = audioFile.replayGainTrackAdjustment,
|
||||||
replayGainAlbumAdjustment = rawSong.replayGainAlbumAdjustment,
|
replayGainAlbumAdjustment = audioFile.replayGainAlbumAdjustment,
|
||||||
track = rawSong.track,
|
track = audioFile.track,
|
||||||
disc = rawSong.disc,
|
disc = audioFile.disc,
|
||||||
subtitle = rawSong.subtitle,
|
subtitle = audioFile.subtitle,
|
||||||
date = rawSong.date,
|
date = audioFile.date,
|
||||||
albumMusicBrainzId = rawSong.albumMusicBrainzId,
|
albumMusicBrainzId = audioFile.albumMusicBrainzId,
|
||||||
albumName = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" },
|
albumName = requireNotNull(audioFile.albumName) { "Invalid raw: No album name" },
|
||||||
albumSortName = rawSong.albumSortName,
|
albumSortName = audioFile.albumSortName,
|
||||||
releaseTypes = rawSong.releaseTypes,
|
releaseTypes = audioFile.releaseTypes,
|
||||||
artistMusicBrainzIds = rawSong.artistMusicBrainzIds,
|
artistMusicBrainzIds = audioFile.artistMusicBrainzIds,
|
||||||
artistNames = rawSong.artistNames,
|
artistNames = audioFile.artistNames,
|
||||||
artistSortNames = rawSong.artistSortNames,
|
artistSortNames = audioFile.artistSortNames,
|
||||||
albumArtistMusicBrainzIds = rawSong.albumArtistMusicBrainzIds,
|
albumArtistMusicBrainzIds = audioFile.albumArtistMusicBrainzIds,
|
||||||
albumArtistNames = rawSong.albumArtistNames,
|
albumArtistNames = audioFile.albumArtistNames,
|
||||||
albumArtistSortNames = rawSong.albumArtistSortNames,
|
albumArtistSortNames = audioFile.albumArtistSortNames,
|
||||||
genreNames = rawSong.genreNames)
|
genreNames = audioFile.genreNames)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,12 @@ import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import org.oxycblt.auxio.music.device.RawSong
|
import org.oxycblt.auxio.music.stack.AudioFile
|
||||||
import org.oxycblt.auxio.music.stack.fs.DeviceFile
|
import org.oxycblt.auxio.music.stack.DeviceFile
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
interface TagResult {
|
interface TagResult {
|
||||||
class Hit(val rawSong: RawSong) : TagResult
|
class Hit(val audioFile: AudioFile) : TagResult
|
||||||
|
|
||||||
class Miss(val file: DeviceFile) : TagResult
|
class Miss(val file: DeviceFile) : TagResult
|
||||||
}
|
}
|
||||||
|
@ -76,9 +76,9 @@ constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val textTags = TextTags(metadata)
|
val textTags = TextTags(metadata)
|
||||||
val rawSong = RawSong(file = input)
|
val audioFile = AudioFile(deviceFile = input)
|
||||||
tagInterpreter2.interpretOn(textTags, rawSong)
|
tagInterpreter2.interpretOn(textTags, audioFile)
|
||||||
collector.emit(TagResult.Hit(rawSong))
|
collector.emit(TagResult.Hit(audioFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun noMetadata(input: DeviceFile) {
|
private suspend fun noMetadata(input: DeviceFile) {
|
||||||
|
|
|
@ -21,13 +21,13 @@ package org.oxycblt.auxio.music.stack.extractor
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import androidx.media3.exoplayer.MetadataRetriever
|
import androidx.media3.exoplayer.MetadataRetriever
|
||||||
import javax.inject.Inject
|
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.music.info.Date
|
||||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on
|
* An processing abstraction over the [MetadataRetriever] and [TextTags] workflow that operates on
|
||||||
* [RawSong] instances.
|
* [AudioFile] instances.
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
|
@ -35,31 +35,31 @@ interface TagInterpreter {
|
||||||
/**
|
/**
|
||||||
* Poll to see if this worker is done processing.
|
* 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 {
|
class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
||||||
override fun interpretOn(textTags: TextTags, rawSong: RawSong) {
|
override fun interpretOn(textTags: TextTags, audioFile: AudioFile) {
|
||||||
populateWithId3v2(rawSong, textTags.id3v2)
|
populateWithId3v2(audioFile, textTags.id3v2)
|
||||||
populateWithVorbis(rawSong, textTags.vorbis)
|
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
|
// Song
|
||||||
(textFrames["TXXX:musicbrainz release track id"]
|
(textFrames["TXXX:musicbrainz release track id"]
|
||||||
?: textFrames["TXXX:musicbrainz_releasetrackid"])
|
?: textFrames["TXXX:musicbrainz_releasetrackid"])
|
||||||
?.let { rawSong.musicBrainzId = it.first() }
|
?.let { audioFile.musicBrainzId = it.first() }
|
||||||
textFrames["TIT2"]?.let { rawSong.name = it.first() }
|
textFrames["TIT2"]?.let { audioFile.name = it.first() }
|
||||||
textFrames["TSOT"]?.let { rawSong.sortName = it.first() }
|
textFrames["TSOT"]?.let { audioFile.sortName = it.first() }
|
||||||
|
|
||||||
// Track.
|
// 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.
|
// Disc and it's subtitle name.
|
||||||
textFrames["TPOS"]?.run { first().parseId3v2PositionField() }?.let { rawSong.disc = it }
|
textFrames["TPOS"]?.run { first().parseId3v2PositionField() }?.let { audioFile.disc = it }
|
||||||
textFrames["TSST"]?.let { rawSong.subtitle = it.first() }
|
textFrames["TSST"]?.let { audioFile.subtitle = it.first() }
|
||||||
|
|
||||||
// Dates are somewhat complicated, as not only did their semantics change from a flat year
|
// 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
|
// 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["TDRC"]?.run { Date.from(first()) }
|
||||||
?: textFrames["TDRL"]?.run { Date.from(first()) }
|
?: textFrames["TDRL"]?.run { Date.from(first()) }
|
||||||
?: parseId3v23Date(textFrames))
|
?: parseId3v23Date(textFrames))
|
||||||
?.let { rawSong.date = it }
|
?.let { audioFile.date = it }
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
(textFrames["TXXX:musicbrainz album id"] ?: textFrames["TXXX:musicbrainz_albumid"])?.let {
|
(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["TALB"]?.let { audioFile.albumName = it.first() }
|
||||||
textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() }
|
textFrames["TSOA"]?.let { audioFile.albumSortName = it.first() }
|
||||||
(textFrames["TXXX:musicbrainz album type"]
|
(textFrames["TXXX:musicbrainz album type"]
|
||||||
?: textFrames["TXXX:releasetype"]
|
?: textFrames["TXXX:releasetype"]
|
||||||
?:
|
?:
|
||||||
// This is a non-standard iTunes extension
|
// This is a non-standard iTunes extension
|
||||||
textFrames["GRP1"])
|
textFrames["GRP1"])
|
||||||
?.let { rawSong.releaseTypes = it }
|
?.let { audioFile.releaseTypes = it }
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
(textFrames["TXXX:musicbrainz artist id"] ?: textFrames["TXXX:musicbrainz_artistid"])?.let {
|
(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 {
|
(textFrames["TXXX:artists"] ?: textFrames["TPE1"] ?: textFrames["TXXX:artist"])?.let {
|
||||||
rawSong.artistNames = it
|
audioFile.artistNames = it
|
||||||
}
|
}
|
||||||
(textFrames["TXXX:artistssort"]
|
(textFrames["TXXX:artistssort"]
|
||||||
?: textFrames["TXXX:artists_sort"]
|
?: textFrames["TXXX:artists_sort"]
|
||||||
|
@ -105,19 +105,19 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
||||||
?: textFrames["TSOP"]
|
?: textFrames["TSOP"]
|
||||||
?: textFrames["artistsort"]
|
?: textFrames["artistsort"]
|
||||||
?: textFrames["TXXX:artist sort"])
|
?: textFrames["TXXX:artist sort"])
|
||||||
?.let { rawSong.artistSortNames = it }
|
?.let { audioFile.artistSortNames = it }
|
||||||
|
|
||||||
// Album artist
|
// Album artist
|
||||||
(textFrames["TXXX:musicbrainz album artist id"]
|
(textFrames["TXXX:musicbrainz album artist id"]
|
||||||
?: textFrames["TXXX:musicbrainz_albumartistid"])
|
?: textFrames["TXXX:musicbrainz_albumartistid"])
|
||||||
?.let { rawSong.albumArtistMusicBrainzIds = it }
|
?.let { audioFile.albumArtistMusicBrainzIds = it }
|
||||||
(textFrames["TXXX:albumartists"]
|
(textFrames["TXXX:albumartists"]
|
||||||
?: textFrames["TXXX:album_artists"]
|
?: textFrames["TXXX:album_artists"]
|
||||||
?: textFrames["TXXX:album artists"]
|
?: textFrames["TXXX:album artists"]
|
||||||
?: textFrames["TPE2"]
|
?: textFrames["TPE2"]
|
||||||
?: textFrames["TXXX:albumartist"]
|
?: textFrames["TXXX:albumartist"]
|
||||||
?: textFrames["TXXX:album artist"])
|
?: textFrames["TXXX:album artist"])
|
||||||
?.let { rawSong.albumArtistNames = it }
|
?.let { audioFile.albumArtistNames = it }
|
||||||
(textFrames["TXXX:albumartistssort"]
|
(textFrames["TXXX:albumartistssort"]
|
||||||
?: textFrames["TXXX:albumartists_sort"]
|
?: textFrames["TXXX:albumartists_sort"]
|
||||||
?: textFrames["TXXX:albumartists sort"]
|
?: textFrames["TXXX:albumartists sort"]
|
||||||
|
@ -125,10 +125,10 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
||||||
// This is a non-standard iTunes extension
|
// This is a non-standard iTunes extension
|
||||||
?: textFrames["TSO2"]
|
?: textFrames["TSO2"]
|
||||||
?: textFrames["TXXX:album artist sort"])
|
?: textFrames["TXXX:album artist sort"])
|
||||||
?.let { rawSong.albumArtistSortNames = it }
|
?.let { audioFile.albumArtistSortNames = it }
|
||||||
|
|
||||||
// Genre
|
// Genre
|
||||||
textFrames["TCON"]?.let { rawSong.genreNames = it }
|
textFrames["TCON"]?.let { audioFile.genreNames = it }
|
||||||
|
|
||||||
// Compilation Flag
|
// Compilation Flag
|
||||||
(textFrames["TCMP"] // This is a non-standard itunes extension
|
(textFrames["TCMP"] // This is a non-standard itunes extension
|
||||||
|
@ -137,17 +137,17 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
||||||
// Ignore invalid instances of this tag
|
// Ignore invalid instances of this tag
|
||||||
if (it.size != 1 || it[0] != "1") return@let
|
if (it.size != 1 || it[0] != "1") return@let
|
||||||
// Change the metadata to be a compilation album made by "Various Artists"
|
// Change the metadata to be a compilation album made by "Various Artists"
|
||||||
rawSong.albumArtistNames =
|
audioFile.albumArtistNames =
|
||||||
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
audioFile.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
||||||
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
audioFile.releaseTypes = audioFile.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplayGain information
|
// ReplayGain information
|
||||||
textFrames["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()?.let {
|
textFrames["TXXX:replaygain_track_gain"]?.parseReplayGainAdjustment()?.let {
|
||||||
rawSong.replayGainTrackAdjustment = it
|
audioFile.replayGainTrackAdjustment = it
|
||||||
}
|
}
|
||||||
textFrames["TXXX:replaygain_album_gain"]?.parseReplayGainAdjustment()?.let {
|
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
|
// Song
|
||||||
(comments["musicbrainz_releasetrackid"] ?: comments["musicbrainz release track id"])?.let {
|
(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["title"]?.let { audioFile.name = it.first() }
|
||||||
comments["titlesort"]?.let { rawSong.sortName = it.first() }
|
comments["titlesort"]?.let { audioFile.sortName = it.first() }
|
||||||
|
|
||||||
// Track.
|
// Track.
|
||||||
parseVorbisPositionField(
|
parseVorbisPositionField(
|
||||||
comments["tracknumber"]?.first(),
|
comments["tracknumber"]?.first(),
|
||||||
(comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first())
|
(comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first())
|
||||||
?.let { rawSong.track = it }
|
?.let { audioFile.track = it }
|
||||||
|
|
||||||
// Disc and it's subtitle name.
|
// Disc and it's subtitle name.
|
||||||
parseVorbisPositionField(
|
parseVorbisPositionField(
|
||||||
comments["discnumber"]?.first(),
|
comments["discnumber"]?.first(),
|
||||||
(comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first())
|
(comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first())
|
||||||
?.let { rawSong.disc = it }
|
?.let { audioFile.disc = it }
|
||||||
comments["discsubtitle"]?.let { rawSong.subtitle = it.first() }
|
comments["discsubtitle"]?.let { audioFile.subtitle = it.first() }
|
||||||
|
|
||||||
// Vorbis dates are less complicated, but there are still several types
|
// Vorbis dates are less complicated, but there are still several types
|
||||||
// Our hierarchy for dates is as such:
|
// Our hierarchy for dates is as such:
|
||||||
|
@ -215,58 +215,58 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
||||||
(comments["originaldate"]?.run { Date.from(first()) }
|
(comments["originaldate"]?.run { Date.from(first()) }
|
||||||
?: comments["date"]?.run { Date.from(first()) }
|
?: comments["date"]?.run { Date.from(first()) }
|
||||||
?: comments["year"]?.run { Date.from(first()) })
|
?: comments["year"]?.run { Date.from(first()) })
|
||||||
?.let { rawSong.date = it }
|
?.let { audioFile.date = it }
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
(comments["musicbrainz_albumid"] ?: comments["musicbrainz album id"])?.let {
|
(comments["musicbrainz_albumid"] ?: comments["musicbrainz album id"])?.let {
|
||||||
rawSong.albumMusicBrainzId = it.first()
|
audioFile.albumMusicBrainzId = it.first()
|
||||||
}
|
}
|
||||||
comments["album"]?.let { rawSong.albumName = it.first() }
|
comments["album"]?.let { audioFile.albumName = it.first() }
|
||||||
comments["albumsort"]?.let { rawSong.albumSortName = it.first() }
|
comments["albumsort"]?.let { audioFile.albumSortName = it.first() }
|
||||||
(comments["releasetype"] ?: comments["musicbrainz album type"])?.let {
|
(comments["releasetype"] ?: comments["musicbrainz album type"])?.let {
|
||||||
rawSong.releaseTypes = it
|
audioFile.releaseTypes = it
|
||||||
}
|
}
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
(comments["musicbrainz_artistid"] ?: comments["musicbrainz artist id"])?.let {
|
(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["artistssort"]
|
||||||
?: comments["artists_sort"]
|
?: comments["artists_sort"]
|
||||||
?: comments["artists sort"]
|
?: comments["artists sort"]
|
||||||
?: comments["artistsort"]
|
?: comments["artistsort"]
|
||||||
?: comments["artist sort"])
|
?: comments["artist sort"])
|
||||||
?.let { rawSong.artistSortNames = it }
|
?.let { audioFile.artistSortNames = it }
|
||||||
|
|
||||||
// Album artist
|
// Album artist
|
||||||
(comments["musicbrainz_albumartistid"] ?: comments["musicbrainz album artist id"])?.let {
|
(comments["musicbrainz_albumartistid"] ?: comments["musicbrainz album artist id"])?.let {
|
||||||
rawSong.albumArtistMusicBrainzIds = it
|
audioFile.albumArtistMusicBrainzIds = it
|
||||||
}
|
}
|
||||||
(comments["albumartists"]
|
(comments["albumartists"]
|
||||||
?: comments["album_artists"]
|
?: comments["album_artists"]
|
||||||
?: comments["album artists"]
|
?: comments["album artists"]
|
||||||
?: comments["albumartist"]
|
?: comments["albumartist"]
|
||||||
?: comments["album artist"])
|
?: comments["album artist"])
|
||||||
?.let { rawSong.albumArtistNames = it }
|
?.let { audioFile.albumArtistNames = it }
|
||||||
(comments["albumartistssort"]
|
(comments["albumartistssort"]
|
||||||
?: comments["albumartists_sort"]
|
?: comments["albumartists_sort"]
|
||||||
?: comments["albumartists sort"]
|
?: comments["albumartists sort"]
|
||||||
?: comments["albumartistsort"]
|
?: comments["albumartistsort"]
|
||||||
?: comments["album artist sort"])
|
?: comments["album artist sort"])
|
||||||
?.let { rawSong.albumArtistSortNames = it }
|
?.let { audioFile.albumArtistSortNames = it }
|
||||||
|
|
||||||
// Genre
|
// Genre
|
||||||
comments["genre"]?.let { rawSong.genreNames = it }
|
comments["genre"]?.let { audioFile.genreNames = it }
|
||||||
|
|
||||||
// Compilation Flag
|
// Compilation Flag
|
||||||
(comments["compilation"] ?: comments["itunescompilation"])?.let {
|
(comments["compilation"] ?: comments["itunescompilation"])?.let {
|
||||||
// Ignore invalid instances of this tag
|
// Ignore invalid instances of this tag
|
||||||
if (it.size != 1 || it[0] != "1") return@let
|
if (it.size != 1 || it[0] != "1") return@let
|
||||||
// Change the metadata to be a compilation album made by "Various Artists"
|
// Change the metadata to be a compilation album made by "Various Artists"
|
||||||
rawSong.albumArtistNames =
|
audioFile.albumArtistNames =
|
||||||
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
audioFile.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
||||||
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
audioFile.releaseTypes = audioFile.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplayGain information
|
// ReplayGain information
|
||||||
|
@ -278,10 +278,10 @@ class TagInterpreterImpl @Inject constructor() : TagInterpreter {
|
||||||
// tags anyway.
|
// tags anyway.
|
||||||
(comments["r128_track_gain"]?.parseR128Adjustment()
|
(comments["r128_track_gain"]?.parseR128Adjustment()
|
||||||
?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment())
|
?: comments["replaygain_track_gain"]?.parseReplayGainAdjustment())
|
||||||
?.let { rawSong.replayGainTrackAdjustment = it }
|
?.let { audioFile.replayGainTrackAdjustment = it }
|
||||||
(comments["r128_album_gain"]?.parseR128Adjustment()
|
(comments["r128_album_gain"]?.parseR128Adjustment()
|
||||||
?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment())
|
?: comments["replaygain_album_gain"]?.parseReplayGainAdjustment())
|
||||||
?.let { rawSong.replayGainAlbumAdjustment = it }
|
?.let { audioFile.replayGainAlbumAdjustment = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<String>.parseR128Adjustment() =
|
private fun List<String>.parseR128Adjustment() =
|
||||||
|
|
|
@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.emitAll
|
||||||
import kotlinx.coroutines.flow.flatMapMerge
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
import kotlinx.coroutines.flow.flattenMerge
|
import kotlinx.coroutines.flow.flattenMerge
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import org.oxycblt.auxio.music.stack.DeviceFile
|
||||||
|
|
||||||
interface DeviceFiles {
|
interface DeviceFiles {
|
||||||
fun explore(uris: Flow<Uri>): Flow<DeviceFile>
|
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.MusicType
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
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.info.Name
|
||||||
|
|
||||||
class PlaylistImpl
|
class PlaylistImpl
|
||||||
|
|
|
@ -24,7 +24,7 @@ import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
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.info.Name
|
||||||
import timber.log.Timber as L
|
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.MusicRepository
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
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.info.Name
|
||||||
import org.oxycblt.auxio.music.service.MediaSessionUID
|
import org.oxycblt.auxio.music.service.MediaSessionUID
|
||||||
import org.oxycblt.auxio.music.service.MusicBrowser
|
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.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicType
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.music.Song
|
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.music.user.UserLibrary
|
||||||
import org.oxycblt.auxio.playback.PlaySong
|
import org.oxycblt.auxio.playback.PlaySong
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
|
|
|
@ -31,7 +31,7 @@ import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
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.info.Date
|
||||||
import org.oxycblt.auxio.music.stack.cache.TagDao
|
import org.oxycblt.auxio.music.stack.cache.TagDao
|
||||||
import org.oxycblt.auxio.music.stack.cache.Tags
|
import org.oxycblt.auxio.music.stack.cache.Tags
|
||||||
|
@ -48,13 +48,13 @@ class CacheRepositoryTest {
|
||||||
coVerifyAll { dao.readSongs() }
|
coVerifyAll { dao.readSongs() }
|
||||||
assertFalse(cache.invalidated)
|
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))
|
assertTrue(cache.populate(songA))
|
||||||
assertEquals(RAW_SONG_A, songA)
|
assertEquals(RAW_SONG_A, songA)
|
||||||
|
|
||||||
assertFalse(cache.invalidated)
|
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))
|
assertTrue(cache.populate(songB))
|
||||||
assertEquals(RAW_SONG_B, songB)
|
assertEquals(RAW_SONG_B, songB)
|
||||||
|
|
||||||
|
@ -72,14 +72,14 @@ class CacheRepositoryTest {
|
||||||
coVerifyAll { dao.readSongs() }
|
coVerifyAll { dao.readSongs() }
|
||||||
assertFalse(cache.invalidated)
|
assertFalse(cache.invalidated)
|
||||||
|
|
||||||
val nullStart = RawSong(mediaStoreId = 0, dateAdded = 0, dateModified = 0)
|
val nullStart = AudioFile(mediaStoreId = 0, dateAdded = 0, dateModified = 0)
|
||||||
val nullEnd = RawSong(mediaStoreId = 0, dateAdded = 0, dateModified = 0)
|
val nullEnd = AudioFile(mediaStoreId = 0, dateAdded = 0, dateModified = 0)
|
||||||
assertFalse(cache.populate(nullStart))
|
assertFalse(cache.populate(nullStart))
|
||||||
assertEquals(nullStart, nullEnd)
|
assertEquals(nullStart, nullEnd)
|
||||||
|
|
||||||
assertTrue(cache.invalidated)
|
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))
|
assertTrue(cache.populate(songB))
|
||||||
assertEquals(RAW_SONG_B, songB)
|
assertEquals(RAW_SONG_B, songB)
|
||||||
|
|
||||||
|
@ -179,7 +179,7 @@ class CacheRepositoryTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
val RAW_SONG_A =
|
val RAW_SONG_A =
|
||||||
RawSong(
|
AudioFile(
|
||||||
mediaStoreId = 0,
|
mediaStoreId = 0,
|
||||||
dateAdded = 1,
|
dateAdded = 1,
|
||||||
dateModified = 2,
|
dateModified = 2,
|
||||||
|
@ -237,7 +237,7 @@ class CacheRepositoryTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
val RAW_SONG_B =
|
val RAW_SONG_B =
|
||||||
RawSong(
|
AudioFile(
|
||||||
mediaStoreId = 9,
|
mediaStoreId = 9,
|
||||||
dateAdded = 10,
|
dateAdded = 10,
|
||||||
dateModified = 11,
|
dateModified = 11,
|
||||||
|
|
|
@ -26,11 +26,11 @@ import org.junit.Assert.assertNotEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicType
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.auxio.music.device.AlbumImpl
|
import org.oxycblt.auxio.music.model.AlbumImpl
|
||||||
import org.oxycblt.auxio.music.device.ArtistImpl
|
import org.oxycblt.auxio.music.model.ArtistImpl
|
||||||
import org.oxycblt.auxio.music.device.DeviceLibraryImpl
|
import org.oxycblt.auxio.music.model.DeviceLibraryImpl
|
||||||
import org.oxycblt.auxio.music.device.GenreImpl
|
import org.oxycblt.auxio.music.model.GenreImpl
|
||||||
import org.oxycblt.auxio.music.device.SongImpl
|
import org.oxycblt.auxio.music.model.SongImpl
|
||||||
import org.oxycblt.auxio.music.stack.fs.Components
|
import org.oxycblt.auxio.music.stack.fs.Components
|
||||||
import org.oxycblt.auxio.music.stack.fs.Path
|
import org.oxycblt.auxio.music.stack.fs.Path
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue