in-progress interpreter refactor
Will force-rewrite at several points.
This commit is contained in:
parent
85bd1f0062
commit
a784f73c5e
8 changed files with 408 additions and 368 deletions
|
@ -0,0 +1,42 @@
|
|||
package org.oxycblt.auxio.music.device
|
||||
|
||||
|
||||
interface AlbumTree {
|
||||
fun register(linkedSong: ArtistTree.LinkedSong): LinkedSong
|
||||
fun resolve(): Collection<AlbumImpl>
|
||||
|
||||
data class LinkedSong(
|
||||
val linkedArtistSong: ArtistTree.LinkedSong,
|
||||
val album: Linked<AlbumImpl, SongImpl>
|
||||
)
|
||||
}
|
||||
|
||||
interface ArtistTree {
|
||||
fun register(preSong: GenreTree.LinkedSong): LinkedSong
|
||||
fun resolve(): Collection<ArtistImpl>
|
||||
|
||||
data class LinkedSong(
|
||||
val linkedGenreSong: GenreTree.LinkedSong,
|
||||
val linkedAlbum: LinkedAlbum,
|
||||
val artists: Linked<List<ArtistImpl>, SongImpl>
|
||||
)
|
||||
|
||||
data class LinkedAlbum(
|
||||
val preAlbum: PreAlbum,
|
||||
val artists: Linked<List<ArtistImpl>, AlbumImpl>
|
||||
)
|
||||
}
|
||||
|
||||
interface GenreTree {
|
||||
fun register(preSong: PreSong): 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
|
||||
}
|
|
@ -127,7 +127,25 @@ interface DeviceLibrary {
|
|||
processedSongs: Channel<RawSong>,
|
||||
separators: Separators,
|
||||
nameFactory: Name.Known.Factory
|
||||
): DeviceLibraryImpl
|
||||
): DeviceLibrary
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceLibraryFactoryImpl2 @Inject constructor(
|
||||
val interpreterFactory: Interpreter.Factory
|
||||
) : DeviceLibrary.Factory {
|
||||
override suspend fun create(
|
||||
rawSongs: Channel<RawSong>,
|
||||
processedSongs: Channel<RawSong>,
|
||||
separators: Separators,
|
||||
nameFactory: Name.Known.Factory
|
||||
): DeviceLibrary {
|
||||
val interpreter = interpreterFactory.create(nameFactory, separators)
|
||||
rawSongs.forEachWithTimeout { rawSong ->
|
||||
interpreter.consume(rawSong)
|
||||
processedSongs.sendWithTimeout(rawSong)
|
||||
}
|
||||
return interpreter.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,7 +155,7 @@ class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory {
|
|||
processedSongs: Channel<RawSong>,
|
||||
separators: Separators,
|
||||
nameFactory: Name.Known.Factory
|
||||
): DeviceLibraryImpl {
|
||||
): DeviceLibrary {
|
||||
val songGrouping = mutableMapOf<Music.UID, SongImpl>()
|
||||
val albumGrouping = mutableMapOf<String?, MutableMap<UUID?, Grouping<RawAlbum, SongImpl>>>()
|
||||
val artistGrouping = mutableMapOf<String?, MutableMap<UUID?, Grouping<RawArtist, Music>>>()
|
||||
|
@ -185,9 +203,7 @@ class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory {
|
|||
|
||||
// Now that all songs are processed, also process albums and group them into their
|
||||
// respective artists.
|
||||
pruneMusicBrainzIdTree(albumGrouping) { old, new ->
|
||||
compareSongTracks(old, new)
|
||||
}
|
||||
pruneMusicBrainzIdTree(albumGrouping) { old, new -> compareSongTracks(old, new) }
|
||||
val albums = flattenMusicBrainzIdTree(albumGrouping) { AlbumImpl(it, nameFactory) }
|
||||
for (album in albums) {
|
||||
for (rawArtist in album.rawArtists) {
|
||||
|
|
|
@ -26,5 +26,6 @@ import dagger.hilt.components.SingletonComponent
|
|||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface DeviceModule {
|
||||
@Binds fun deviceLibraryFactory(factory: DeviceLibraryFactoryImpl): DeviceLibrary.Factory
|
||||
@Binds fun deviceLibraryFactory(factory: DeviceLibraryFactoryImpl2): DeviceLibrary.Factory
|
||||
@Binds fun interpreterFactory(factory: InterpreterFactoryImpl): Interpreter.Factory
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
package org.oxycblt.auxio.music.device
|
||||
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.image.extractor.Cover
|
||||
import org.oxycblt.auxio.image.extractor.ParentCover
|
||||
import org.oxycblt.auxio.list.sort.Sort
|
||||
import org.oxycblt.auxio.music.Album
|
||||
|
@ -28,346 +27,95 @@ import org.oxycblt.auxio.music.Genre
|
|||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicType
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.fs.MimeType
|
||||
import org.oxycblt.auxio.music.fs.toAlbumCoverUri
|
||||
import org.oxycblt.auxio.music.fs.toAudioUri
|
||||
import org.oxycblt.auxio.music.fs.toSongCoverUri
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.music.metadata.Separators
|
||||
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.positiveOrNull
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
import org.oxycblt.auxio.util.update
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Library-backed implementation of [Song].
|
||||
*
|
||||
* @param rawSong The [RawSong] to derive the member data from.
|
||||
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
|
||||
* @param separators The [Separators] to parse multi-value tags with.
|
||||
* @param linkedSong The completed [LinkedSong] all metadata van be inferred from
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class SongImpl(
|
||||
private val rawSong: RawSong,
|
||||
private val nameFactory: Name.Known.Factory,
|
||||
private val separators: Separators
|
||||
) : Song {
|
||||
class SongImpl(linkedSong: LinkedSong) : Song {
|
||||
private val preSong = linkedSong.preSong
|
||||
|
||||
override val uid =
|
||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicType.SONGS, it) }
|
||||
preSong.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.SONGS, it) }
|
||||
?: Music.UID.auxio(MusicType.SONGS) {
|
||||
// Song UIDs are based on the raw data without parsing so that they remain
|
||||
// consistent across music setting changes. Parents are not held up to the
|
||||
// same standard since grouping is already inherently linked to settings.
|
||||
update(rawSong.name)
|
||||
update(rawSong.albumName)
|
||||
update(rawSong.date)
|
||||
update(preSong.rawName)
|
||||
update(preSong.preAlbum.rawName)
|
||||
update(preSong.date)
|
||||
|
||||
update(rawSong.track)
|
||||
update(rawSong.disc)
|
||||
update(preSong.track)
|
||||
update(preSong.disc?.number)
|
||||
|
||||
update(rawSong.artistNames)
|
||||
update(rawSong.albumArtistNames)
|
||||
update(preSong.preArtists.map { it.rawName })
|
||||
update(preSong.preAlbum.preArtists.map { it.rawName })
|
||||
}
|
||||
override val name =
|
||||
nameFactory.parse(
|
||||
requireNotNull(rawSong.name) { "Invalid raw ${rawSong.path}: No title" },
|
||||
rawSong.sortName)
|
||||
override val name = preSong.name
|
||||
override val track = preSong.track
|
||||
override val disc = preSong.disc
|
||||
override val date = preSong.date
|
||||
override val uri = preSong.uri
|
||||
override val cover = preSong.cover
|
||||
override val path = preSong.path
|
||||
override val mimeType = preSong.mimeType
|
||||
override val size = preSong.size
|
||||
override val durationMs = preSong.durationMs
|
||||
override val replayGainAdjustment = preSong.replayGainAdjustment
|
||||
override val dateAdded = preSong.dateAdded
|
||||
override val album = linkedSong.album.resolve(this)
|
||||
override val artists = linkedSong.artists.resolve(this)
|
||||
override val genres = linkedSong.genres.resolve(this)
|
||||
|
||||
override val track = rawSong.track
|
||||
override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }
|
||||
override val date = rawSong.date
|
||||
override val uri =
|
||||
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }.toAudioUri()
|
||||
override val path = requireNotNull(rawSong.path) { "Invalid raw ${rawSong.path}: No path" }
|
||||
override val mimeType =
|
||||
MimeType(
|
||||
fromExtension =
|
||||
requireNotNull(rawSong.extensionMimeType) {
|
||||
"Invalid raw ${rawSong.path}: No mime type"
|
||||
},
|
||||
fromFormat = null)
|
||||
override val size = requireNotNull(rawSong.size) { "Invalid raw ${rawSong.path}: No size" }
|
||||
override val durationMs =
|
||||
requireNotNull(rawSong.durationMs) { "Invalid raw ${rawSong.path}: No duration" }
|
||||
override val replayGainAdjustment =
|
||||
ReplayGainAdjustment(
|
||||
track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment)
|
||||
|
||||
override val dateAdded =
|
||||
requireNotNull(rawSong.dateAdded) { "Invalid raw ${rawSong.path}: No date added" }
|
||||
|
||||
private var _album: AlbumImpl? = null
|
||||
override val album: Album
|
||||
get() = unlikelyToBeNull(_album)
|
||||
|
||||
private val _artists = mutableListOf<ArtistImpl>()
|
||||
override val artists: List<Artist>
|
||||
get() = _artists
|
||||
|
||||
private val _genres = mutableListOf<GenreImpl>()
|
||||
override val genres: List<Genre>
|
||||
get() = _genres
|
||||
|
||||
override val cover =
|
||||
rawSong.coverPerceptualHash?.let {
|
||||
// We were able to confirm that the song had a parsable cover and can be used on
|
||||
// a per-song basis. Otherwise, just fall back to a per-album cover instead, as
|
||||
// it implies either a cover.jpg pattern is used (likely) or ExoPlayer does not
|
||||
// support the cover metadata of a given spec (unlikely).
|
||||
Cover.Embedded(
|
||||
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }
|
||||
.toSongCoverUri(),
|
||||
uri,
|
||||
it)
|
||||
} ?: Cover.External(requireNotNull(rawSong.albumMediaStoreId).toAlbumCoverUri())
|
||||
|
||||
/**
|
||||
* The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an
|
||||
* [Album].
|
||||
*/
|
||||
val rawAlbum: RawAlbum
|
||||
|
||||
/**
|
||||
* The [RawArtist] instances collated by the [Song]. The artists of the song take priority,
|
||||
* followed by the album artists. If there are no artists, this field will be a single "unknown"
|
||||
* [RawArtist]. This can be used to group up [Song]s into an [Artist].
|
||||
*/
|
||||
val rawArtists: List<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(
|
||||
mediaStoreId =
|
||||
requireNotNull(rawSong.albumMediaStoreId) {
|
||||
"Invalid raw ${rawSong.path}: No album id"
|
||||
},
|
||||
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
|
||||
name =
|
||||
requireNotNull(rawSong.albumName) {
|
||||
"Invalid raw ${rawSong.path}: No album name"
|
||||
},
|
||||
sortName = rawSong.albumSortName,
|
||||
releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)),
|
||||
rawArtists =
|
||||
rawAlbumArtists
|
||||
.ifEmpty { rawIndividualArtists }
|
||||
.ifEmpty { listOf(RawArtist()) })
|
||||
|
||||
rawArtists =
|
||||
rawIndividualArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(RawArtist()) }
|
||||
|
||||
val genreNames =
|
||||
(rawSong.genreNames.parseId3GenreNames() ?: separators.split(rawSong.genreNames))
|
||||
rawGenres =
|
||||
genreNames
|
||||
.map { RawGenre(it) }
|
||||
.distinctBy { it.name?.lowercase() }
|
||||
.ifEmpty { listOf(RawGenre()) }
|
||||
|
||||
hashCode = 31 * hashCode + rawSong.hashCode()
|
||||
hashCode = 31 * hashCode + nameFactory.hashCode()
|
||||
}
|
||||
private val hashCode = 31 * uid.hashCode() + preSong.hashCode()
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
// Since equality on public-facing music models is not identical to the tag equality,
|
||||
// we just compare raw instances and how they are interpreted.
|
||||
override fun equals(other: Any?) =
|
||||
other is SongImpl &&
|
||||
uid == other.uid &&
|
||||
nameFactory == other.nameFactory &&
|
||||
separators == other.separators &&
|
||||
rawSong == other.rawSong
|
||||
preSong == other.preSong
|
||||
|
||||
override fun toString() = "Song(uid=$uid, name=$name)"
|
||||
|
||||
/**
|
||||
* Links this [Song] with a parent [Album].
|
||||
*
|
||||
* @param album The parent [Album] to link to.
|
||||
*/
|
||||
fun link(album: AlbumImpl) {
|
||||
_album = album
|
||||
}
|
||||
|
||||
/**
|
||||
* Links this [Song] with a parent [Artist].
|
||||
*
|
||||
* @param artist The parent [Artist] to link to.
|
||||
*/
|
||||
fun link(artist: ArtistImpl) {
|
||||
_artists.add(artist)
|
||||
}
|
||||
|
||||
/**
|
||||
* Links this [Song] with a parent [Genre].
|
||||
*
|
||||
* @param genre The parent [Genre] to link to.
|
||||
*/
|
||||
fun link(genre: GenreImpl) {
|
||||
_genres.add(genre)
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform final validation and organization on this instance.
|
||||
*
|
||||
* @return This instance upcasted to [Song].
|
||||
*/
|
||||
fun finalize(): Song {
|
||||
checkNotNull(_album) { "Malformed song ${path}: No album" }
|
||||
|
||||
check(_artists.isNotEmpty()) { "Malformed song ${path}: No artists" }
|
||||
check(_artists.size == rawArtists.size) {
|
||||
"Malformed song ${path}: Artist grouping mismatch"
|
||||
}
|
||||
for (i in _artists.indices) {
|
||||
// Non-destructively reorder the linked artists so that they align with
|
||||
// the artist ordering within the song metadata.
|
||||
val newIdx = _artists[i].getOriginalPositionIn(rawArtists)
|
||||
val other = _artists[newIdx]
|
||||
_artists[newIdx] = _artists[i]
|
||||
_artists[i] = other
|
||||
}
|
||||
|
||||
check(_genres.isNotEmpty()) { "Malformed song ${path}: No genres" }
|
||||
check(_genres.size == rawGenres.size) { "Malformed song ${path}: Genre grouping mismatch" }
|
||||
for (i in _genres.indices) {
|
||||
// Non-destructively reorder the linked genres so that they align with
|
||||
// the genre ordering within the song metadata.
|
||||
val newIdx = _genres[i].getOriginalPositionIn(rawGenres)
|
||||
val other = _genres[newIdx]
|
||||
_genres[newIdx] = _genres[i]
|
||||
_genres[i] = other
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Library-backed implementation of [Album].
|
||||
*
|
||||
* @param grouping [Grouping] to derive the member data from.
|
||||
* @param nameFactory The [Name.Known.Factory] to interpret name information with.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class AlbumImpl(
|
||||
grouping: Grouping<RawAlbum, SongImpl>,
|
||||
private val nameFactory: Name.Known.Factory
|
||||
) : Album {
|
||||
private val rawAlbum = grouping.raw.inner
|
||||
class AlbumImpl(linkedAlbum: LinkedAlbum) : Album {
|
||||
private val preAlbum = linkedAlbum.preAlbum
|
||||
|
||||
override val uid =
|
||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ALBUMS, it) }
|
||||
preAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicType.ALBUMS, it) }
|
||||
?: Music.UID.auxio(MusicType.ALBUMS) {
|
||||
// Hash based on only names despite the presence of a date to increase stability.
|
||||
// I don't know if there is any situation where an artist will have two albums with
|
||||
// the exact same name, but if there is, I would love to know.
|
||||
update(rawAlbum.name)
|
||||
update(rawAlbum.rawArtists.map { it.name })
|
||||
update(preAlbum.rawName)
|
||||
update(preAlbum.preArtists.map { it.rawName })
|
||||
}
|
||||
override val name = nameFactory.parse(rawAlbum.name, rawAlbum.sortName)
|
||||
override val dates: Date.Range?
|
||||
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
|
||||
override val durationMs: Long
|
||||
override val dateAdded: Long
|
||||
override val cover: ParentCover
|
||||
override val name = preAlbum.name
|
||||
override val releaseType = preAlbum.releaseType
|
||||
override var durationMs = 0L
|
||||
override var dateAdded = 0L
|
||||
override lateinit var cover: ParentCover
|
||||
override var dates: Date.Range? = null
|
||||
|
||||
private val _artists = mutableListOf<ArtistImpl>()
|
||||
override val artists: List<Artist>
|
||||
get() = _artists
|
||||
override val artists = linkedAlbum.artists.resolve(this)
|
||||
override val songs = mutableSetOf<Song>()
|
||||
|
||||
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()
|
||||
}
|
||||
private var hashCode = 31 * uid.hashCode() + preAlbum.hashCode()
|
||||
|
||||
override fun hashCode() = hashCode
|
||||
|
||||
|
@ -376,26 +124,23 @@ class AlbumImpl(
|
|||
override fun equals(other: Any?) =
|
||||
other is AlbumImpl &&
|
||||
uid == other.uid &&
|
||||
rawAlbum == other.rawAlbum &&
|
||||
nameFactory == other.nameFactory &&
|
||||
preAlbum == other.preAlbum &&
|
||||
songs == other.songs
|
||||
|
||||
override fun toString() = "Album(uid=$uid, name=$name)"
|
||||
|
||||
/**
|
||||
* The [RawArtist] instances collated by the [Album]. The album artists of the song take
|
||||
* priority, followed by the artists. If there are no artists, this field will be a single
|
||||
* "unknown" [RawArtist]. This can be used to group up [Album]s into an [Artist].
|
||||
*/
|
||||
val rawArtists = rawAlbum.rawArtists
|
||||
|
||||
/**
|
||||
* Links this [Album] with a parent [Artist].
|
||||
*
|
||||
* @param artist The parent [Artist] to link to.
|
||||
*/
|
||||
fun link(artist: ArtistImpl) {
|
||||
_artists.add(artist)
|
||||
fun link(song: SongImpl) {
|
||||
songs.add(song)
|
||||
hashCode = 31 * hashCode + song.hashCode()
|
||||
durationMs += song.durationMs
|
||||
dateAdded = min(dateAdded, song.dateAdded)
|
||||
if (song.date != null) {
|
||||
dates = dates?.let {
|
||||
if (song.date < it.min) Date.Range(song.date, it.max)
|
||||
else if (song.date > it.max) Date.Range(it.min, song.date)
|
||||
else it
|
||||
} ?: Date.Range(song.date, song.date)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -404,19 +149,6 @@ class AlbumImpl(
|
|||
* @return This instance upcasted to [Album].
|
||||
*/
|
||||
fun finalize(): Album {
|
||||
check(songs.isNotEmpty()) { "Malformed album $name: Empty" }
|
||||
check(_artists.isNotEmpty()) { "Malformed album $name: No artists" }
|
||||
check(_artists.size == rawArtists.size) {
|
||||
"Malformed album $name: Artist grouping mismatch"
|
||||
}
|
||||
for (i in _artists.indices) {
|
||||
// Non-destructively reorder the linked artists so that they align with
|
||||
// the artist ordering within the song metadata.
|
||||
val newIdx = _artists[i].getOriginalPositionIn(rawArtists)
|
||||
val other = _artists[newIdx]
|
||||
_artists[newIdx] = _artists[i]
|
||||
_artists[i] = other
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
@ -465,10 +197,12 @@ class ArtistImpl(
|
|||
albumMap[music.album] = false
|
||||
}
|
||||
}
|
||||
|
||||
is AlbumImpl -> {
|
||||
music.link(this)
|
||||
albumMap[music] = true
|
||||
}
|
||||
|
||||
else -> error("Unexpected input music $music in $name ${music::class.simpleName}")
|
||||
}
|
||||
}
|
||||
|
@ -507,17 +241,6 @@ class ArtistImpl(
|
|||
|
||||
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.
|
||||
|
@ -600,18 +323,6 @@ class GenreImpl(
|
|||
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package org.oxycblt.auxio.music.device
|
||||
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.metadata.Separators
|
||||
import javax.inject.Inject
|
||||
|
||||
interface Interpreter {
|
||||
fun consume(rawSong: RawSong)
|
||||
fun resolve(): DeviceLibrary
|
||||
|
||||
interface Factory {
|
||||
fun create(nameFactory: Name.Known.Factory, separators: Separators): Interpreter
|
||||
}
|
||||
}
|
||||
|
||||
class LinkedSong(val albumLinkedSong: AlbumTree.LinkedSong) {
|
||||
val preSong: PreSong get() = albumLinkedSong.linkedArtistSong.linkedGenreSong.preSong
|
||||
val album: Linked<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 = ArtistTree.LinkedAlbum
|
||||
|
||||
class InterpreterFactoryImpl @Inject constructor(
|
||||
private val songInterpreterFactory: SongInterpreter.Factory,
|
||||
private val albumTree: AlbumTree,
|
||||
private val artistTree: ArtistTree,
|
||||
private val genreTree: GenreTree
|
||||
) : Interpreter.Factory {
|
||||
override fun create(nameFactory: Name.Known.Factory, separators: Separators): Interpreter =
|
||||
InterpreterImpl(
|
||||
songInterpreterFactory.create(nameFactory, separators),
|
||||
albumTree,
|
||||
artistTree,
|
||||
genreTree
|
||||
)
|
||||
}
|
||||
|
||||
private class InterpreterImpl(
|
||||
private val songInterpreter: SongInterpreter,
|
||||
private val albumTree: AlbumTree,
|
||||
private val artistTree: ArtistTree,
|
||||
private val genreTree: GenreTree
|
||||
) : Interpreter {
|
||||
private val songs = mutableListOf<LinkedSong>()
|
||||
|
||||
override fun consume(rawSong: RawSong) {
|
||||
val preSong = songInterpreter.consume(rawSong)
|
||||
val genreLinkedSong = genreTree.register(preSong)
|
||||
val artistLinkedSong = artistTree.register(genreLinkedSong)
|
||||
val albumLinkedSong = albumTree.register(artistLinkedSong)
|
||||
songs.add(LinkedSong(albumLinkedSong))
|
||||
}
|
||||
|
||||
override fun resolve(): DeviceLibrary {
|
||||
val genres = genreTree.resolve()
|
||||
val artists = artistTree.resolve()
|
||||
val albums = albumTree.resolve()
|
||||
val songs = songs.map { SongImpl(it) }
|
||||
return DeviceLibraryImpl(songs, albums, artists, genres)
|
||||
}
|
||||
}
|
51
app/src/main/java/org/oxycblt/auxio/music/device/PreMusic.kt
Normal file
51
app/src/main/java/org/oxycblt/auxio/music/device/PreMusic.kt
Normal file
|
@ -0,0 +1,51 @@
|
|||
package org.oxycblt.auxio.music.device
|
||||
|
||||
import android.net.Uri
|
||||
import org.oxycblt.auxio.image.extractor.Cover
|
||||
import org.oxycblt.auxio.music.fs.MimeType
|
||||
import org.oxycblt.auxio.music.fs.Path
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import java.util.UUID
|
||||
|
||||
data class PreSong(
|
||||
val musicBrainzId: UUID?,
|
||||
val name: Name,
|
||||
val rawName: String?,
|
||||
val track: Int?,
|
||||
val disc: Disc?,
|
||||
val date: Date?,
|
||||
val uri: Uri,
|
||||
val cover: Cover,
|
||||
val path: Path,
|
||||
val mimeType: MimeType,
|
||||
val size: Long,
|
||||
val durationMs: Long,
|
||||
val replayGainAdjustment: ReplayGainAdjustment,
|
||||
val dateAdded: Long,
|
||||
val preAlbum: PreAlbum,
|
||||
val preArtists: List<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,156 @@
|
|||
package org.oxycblt.auxio.music.device
|
||||
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.image.extractor.Cover
|
||||
import org.oxycblt.auxio.music.fs.MimeType
|
||||
import org.oxycblt.auxio.music.fs.toAlbumCoverUri
|
||||
import org.oxycblt.auxio.music.fs.toAudioUri
|
||||
import org.oxycblt.auxio.music.fs.toSongCoverUri
|
||||
import org.oxycblt.auxio.music.info.Disc
|
||||
import org.oxycblt.auxio.music.info.Name
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
import org.oxycblt.auxio.music.metadata.Separators
|
||||
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||
import org.oxycblt.auxio.util.toUuidOrNull
|
||||
|
||||
interface SongInterpreter {
|
||||
fun consume(rawSong: RawSong): PreSong
|
||||
|
||||
interface Factory {
|
||||
fun create(nameFactory: Name.Known.Factory, separators: Separators): SongInterpreter
|
||||
}
|
||||
}
|
||||
|
||||
class SongInterpreterFactory : SongInterpreter.Factory {
|
||||
override fun create(nameFactory: Name.Known.Factory, separators: Separators) =
|
||||
SongInterpreterImpl(nameFactory, separators)
|
||||
}
|
||||
|
||||
class SongInterpreterImpl(
|
||||
private val nameFactory: Name.Known.Factory,
|
||||
private val separators: Separators
|
||||
) : SongInterpreter {
|
||||
override fun consume(rawSong: RawSong): PreSong {
|
||||
val individualPreArtists = makePreArtists(
|
||||
rawSong.artistMusicBrainzIds,
|
||||
rawSong.artistNames,
|
||||
rawSong.artistSortNames
|
||||
)
|
||||
val albumPreArtists = makePreArtists(
|
||||
rawSong.albumArtistMusicBrainzIds,
|
||||
rawSong.albumArtistNames,
|
||||
rawSong.albumArtistSortNames
|
||||
)
|
||||
val preAlbum = makePreAlbum(rawSong, individualPreArtists, albumPreArtists)
|
||||
val rawArtists =
|
||||
individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) }
|
||||
val rawGenres =
|
||||
makePreGenres(rawSong).ifEmpty { listOf(unknownPreGenre()) }
|
||||
val uri = need(rawSong, "uri", rawSong.mediaStoreId).toAudioUri()
|
||||
return PreSong(
|
||||
musicBrainzId = rawSong.musicBrainzId?.toUuidOrNull(),
|
||||
name = nameFactory.parse(need(rawSong, "name", rawSong.name), rawSong.sortName),
|
||||
rawName = rawSong.name,
|
||||
track = rawSong.track,
|
||||
disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) },
|
||||
date = rawSong.date,
|
||||
uri = uri,
|
||||
cover = inferCover(rawSong),
|
||||
path = need(rawSong, "path", rawSong.path),
|
||||
mimeType = MimeType(
|
||||
need(rawSong, "mime type", rawSong.extensionMimeType),
|
||||
null
|
||||
),
|
||||
size = need(rawSong, "size", rawSong.size),
|
||||
durationMs = need(rawSong, "duration", rawSong.durationMs),
|
||||
replayGainAdjustment = ReplayGainAdjustment(
|
||||
rawSong.replayGainTrackAdjustment,
|
||||
rawSong.replayGainAlbumAdjustment,
|
||||
),
|
||||
dateAdded = need(rawSong, "date added", rawSong.dateAdded),
|
||||
preAlbum = preAlbum,
|
||||
preArtists = rawArtists,
|
||||
preGenres = rawGenres
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> need(rawSong: RawSong, what: String, value: T?) =
|
||||
requireNotNull(value) { "Invalid $what for song ${rawSong.path}: No $what" }
|
||||
|
||||
private fun inferCover(rawSong: RawSong): Cover {
|
||||
val uri = need(rawSong, "uri", rawSong.mediaStoreId).toAudioUri()
|
||||
return rawSong.coverPerceptualHash?.let {
|
||||
Cover.Embedded(
|
||||
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.path}: No id" }
|
||||
.toSongCoverUri(),
|
||||
uri,
|
||||
it)
|
||||
} ?: Cover.External(requireNotNull(rawSong.albumMediaStoreId).toAlbumCoverUri())
|
||||
}
|
||||
|
||||
private fun makePreAlbum(
|
||||
rawSong: RawSong,
|
||||
individualPreArtists: List<PreArtist>,
|
||||
albumPreArtists: List<PreArtist>
|
||||
): PreAlbum {
|
||||
val rawAlbumName = need(rawSong, "album name", rawSong.albumName)
|
||||
return PreAlbum(
|
||||
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
|
||||
name = nameFactory.parse(rawAlbumName, rawSong.albumSortName),
|
||||
rawName = rawAlbumName,
|
||||
releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes))
|
||||
?: ReleaseType.Album(null),
|
||||
preArtists =
|
||||
albumPreArtists
|
||||
.ifEmpty { individualPreArtists }
|
||||
.ifEmpty { listOf(unknownPreArtist()) })
|
||||
}
|
||||
|
||||
private fun makePreArtists(
|
||||
rawMusicBrainzIds: List<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(rawSong: RawSong): List<PreGenre> {
|
||||
val genreNames =
|
||||
rawSong.genreNames.parseId3GenreNames() ?: separators.split(rawSong.genreNames)
|
||||
return genreNames.map { makePreGenre(it) }
|
||||
}
|
||||
|
||||
private fun makePreGenre(rawName: String?) =
|
||||
PreGenre(rawName?.let { nameFactory.parse(it, null) } ?: Name.Unknown(R.string.def_genre),
|
||||
rawName)
|
||||
|
||||
private fun unknownPreGenre() =
|
||||
PreGenre(Name.Unknown(R.string.def_genre), null)
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* Copyright (c) 2023 Auxio Prct
|
||||
* Name.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
|
|
Loading…
Reference in a new issue