Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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>,
|
processedSongs: Channel<RawSong>,
|
||||||
separators: Separators,
|
separators: Separators,
|
||||||
nameFactory: Name.Known.Factory
|
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>,
|
processedSongs: Channel<RawSong>,
|
||||||
separators: Separators,
|
separators: Separators,
|
||||||
nameFactory: Name.Known.Factory
|
nameFactory: Name.Known.Factory
|
||||||
): DeviceLibraryImpl {
|
): DeviceLibrary {
|
||||||
val songGrouping = mutableMapOf<Music.UID, SongImpl>()
|
val songGrouping = mutableMapOf<Music.UID, SongImpl>()
|
||||||
val albumGrouping = mutableMapOf<String?, MutableMap<UUID?, Grouping<RawAlbum, SongImpl>>>()
|
val albumGrouping = mutableMapOf<String?, MutableMap<UUID?, Grouping<RawAlbum, SongImpl>>>()
|
||||||
val artistGrouping = mutableMapOf<String?, MutableMap<UUID?, Grouping<RawArtist, Music>>>()
|
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
|
// Now that all songs are processed, also process albums and group them into their
|
||||||
// respective artists.
|
// respective artists.
|
||||||
pruneMusicBrainzIdTree(albumGrouping) { old, new ->
|
pruneMusicBrainzIdTree(albumGrouping) { old, new -> compareSongTracks(old, new) }
|
||||||
compareSongTracks(old, new)
|
|
||||||
}
|
|
||||||
val albums = flattenMusicBrainzIdTree(albumGrouping) { AlbumImpl(it, nameFactory) }
|
val albums = flattenMusicBrainzIdTree(albumGrouping) { AlbumImpl(it, nameFactory) }
|
||||||
for (album in albums) {
|
for (album in albums) {
|
||||||
for (rawArtist in album.rawArtists) {
|
for (rawArtist in album.rawArtists) {
|
||||||
|
@ -214,7 +230,7 @@ class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory {
|
||||||
compareSongDates(old, new)
|
compareSongDates(old, new)
|
||||||
}
|
}
|
||||||
old is AlbumImpl && new is AlbumImpl -> {
|
old is AlbumImpl && new is AlbumImpl -> {
|
||||||
compareAlbumDates(old, new)
|
compareAlbumDates(old, new)
|
||||||
}
|
}
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,5 +26,6 @@ import dagger.hilt.components.SingletonComponent
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface DeviceModule {
|
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
|
package org.oxycblt.auxio.music.device
|
||||||
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.image.extractor.Cover
|
|
||||||
import org.oxycblt.auxio.image.extractor.ParentCover
|
import org.oxycblt.auxio.image.extractor.ParentCover
|
||||||
import org.oxycblt.auxio.list.sort.Sort
|
import org.oxycblt.auxio.list.sort.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
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.Music
|
||||||
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.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.Date
|
||||||
import org.oxycblt.auxio.music.info.Disc
|
|
||||||
import org.oxycblt.auxio.music.info.Name
|
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.positiveOrNull
|
||||||
import org.oxycblt.auxio.util.toUuidOrNull
|
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
|
||||||
import org.oxycblt.auxio.util.update
|
import org.oxycblt.auxio.util.update
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Library-backed implementation of [Song].
|
* Library-backed implementation of [Song].
|
||||||
*
|
*
|
||||||
* @param rawSong The [RawSong] to derive the member data from.
|
* @param linkedSong The completed [LinkedSong] all metadata van be inferred 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)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class SongImpl(
|
class SongImpl(linkedSong: LinkedSong) : Song {
|
||||||
private val rawSong: RawSong,
|
private val preSong = linkedSong.preSong
|
||||||
private val nameFactory: Name.Known.Factory,
|
|
||||||
private val separators: Separators
|
|
||||||
) : Song {
|
|
||||||
override val uid =
|
override val uid =
|
||||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed 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) {
|
?: Music.UID.auxio(MusicType.SONGS) {
|
||||||
// Song UIDs are based on the raw data without parsing so that they remain
|
// 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
|
// consistent across music setting changes. Parents are not held up to the
|
||||||
// same standard since grouping is already inherently linked to settings.
|
// same standard since grouping is already inherently linked to settings.
|
||||||
update(rawSong.name)
|
update(preSong.rawName)
|
||||||
update(rawSong.albumName)
|
update(preSong.preAlbum.rawName)
|
||||||
update(rawSong.date)
|
update(preSong.date)
|
||||||
|
|
||||||
update(rawSong.track)
|
update(preSong.track)
|
||||||
update(rawSong.disc)
|
update(preSong.disc?.number)
|
||||||
|
|
||||||
update(rawSong.artistNames)
|
update(preSong.preArtists.map { it.rawName })
|
||||||
update(rawSong.albumArtistNames)
|
update(preSong.preAlbum.preArtists.map { it.rawName })
|
||||||
}
|
}
|
||||||
override val name =
|
override val name = preSong.name
|
||||||
nameFactory.parse(
|
override val track = preSong.track
|
||||||
requireNotNull(rawSong.name) { "Invalid raw ${rawSong.path}: No title" },
|
override val disc = preSong.disc
|
||||||
rawSong.sortName)
|
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
|
private val hashCode = 31 * uid.hashCode() + preSong.hashCode()
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode() = 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?) =
|
override fun equals(other: Any?) =
|
||||||
other is SongImpl &&
|
other is SongImpl &&
|
||||||
uid == other.uid &&
|
uid == other.uid &&
|
||||||
nameFactory == other.nameFactory &&
|
preSong == other.preSong
|
||||||
separators == other.separators &&
|
|
||||||
rawSong == other.rawSong
|
|
||||||
|
|
||||||
override fun toString() = "Song(uid=$uid, name=$name)"
|
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].
|
* 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)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class AlbumImpl(
|
class AlbumImpl(linkedAlbum: LinkedAlbum) : Album {
|
||||||
grouping: Grouping<RawAlbum, SongImpl>,
|
private val preAlbum = linkedAlbum.preAlbum
|
||||||
private val nameFactory: Name.Known.Factory
|
|
||||||
) : Album {
|
|
||||||
private val rawAlbum = grouping.raw.inner
|
|
||||||
|
|
||||||
override val uid =
|
override val uid =
|
||||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed 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) {
|
?: Music.UID.auxio(MusicType.ALBUMS) {
|
||||||
// Hash based on only names despite the presence of a date to increase stability.
|
// 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
|
// 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.
|
// the exact same name, but if there is, I would love to know.
|
||||||
update(rawAlbum.name)
|
update(preAlbum.rawName)
|
||||||
update(rawAlbum.rawArtists.map { it.name })
|
update(preAlbum.preArtists.map { it.rawName })
|
||||||
}
|
}
|
||||||
override val name = nameFactory.parse(rawAlbum.name, rawAlbum.sortName)
|
override val name = preAlbum.name
|
||||||
override val dates: Date.Range?
|
override val releaseType = preAlbum.releaseType
|
||||||
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
|
override var durationMs = 0L
|
||||||
override val durationMs: Long
|
override var dateAdded = 0L
|
||||||
override val dateAdded: Long
|
override lateinit var cover: ParentCover
|
||||||
override val cover: ParentCover
|
override var dates: Date.Range? = null
|
||||||
|
|
||||||
private val _artists = mutableListOf<ArtistImpl>()
|
override val artists = linkedAlbum.artists.resolve(this)
|
||||||
override val artists: List<Artist>
|
override val songs = mutableSetOf<Song>()
|
||||||
get() = _artists
|
|
||||||
|
|
||||||
override val songs: Set<Song> = grouping.music
|
private var hashCode = 31 * uid.hashCode() + preAlbum.hashCode()
|
||||||
|
|
||||||
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
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
|
@ -375,27 +123,24 @@ class AlbumImpl(
|
||||||
// we just compare raw instances and how they are interpreted.
|
// we just compare raw instances and how they are interpreted.
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
other is AlbumImpl &&
|
other is AlbumImpl &&
|
||||||
uid == other.uid &&
|
uid == other.uid &&
|
||||||
rawAlbum == other.rawAlbum &&
|
preAlbum == other.preAlbum &&
|
||||||
nameFactory == other.nameFactory &&
|
songs == other.songs
|
||||||
songs == other.songs
|
|
||||||
|
|
||||||
override fun toString() = "Album(uid=$uid, name=$name)"
|
override fun toString() = "Album(uid=$uid, name=$name)"
|
||||||
|
|
||||||
/**
|
fun link(song: SongImpl) {
|
||||||
* The [RawArtist] instances collated by the [Album]. The album artists of the song take
|
songs.add(song)
|
||||||
* priority, followed by the artists. If there are no artists, this field will be a single
|
hashCode = 31 * hashCode + song.hashCode()
|
||||||
* "unknown" [RawArtist]. This can be used to group up [Album]s into an [Artist].
|
durationMs += song.durationMs
|
||||||
*/
|
dateAdded = min(dateAdded, song.dateAdded)
|
||||||
val rawArtists = rawAlbum.rawArtists
|
if (song.date != null) {
|
||||||
|
dates = dates?.let {
|
||||||
/**
|
if (song.date < it.min) Date.Range(song.date, it.max)
|
||||||
* Links this [Album] with a parent [Artist].
|
else if (song.date > it.max) Date.Range(it.min, song.date)
|
||||||
*
|
else it
|
||||||
* @param artist The parent [Artist] to link to.
|
} ?: Date.Range(song.date, song.date)
|
||||||
*/
|
}
|
||||||
fun link(artist: ArtistImpl) {
|
|
||||||
_artists.add(artist)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -404,19 +149,6 @@ class AlbumImpl(
|
||||||
* @return This instance upcasted to [Album].
|
* @return This instance upcasted to [Album].
|
||||||
*/
|
*/
|
||||||
fun finalize(): 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
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -465,10 +197,12 @@ class ArtistImpl(
|
||||||
albumMap[music.album] = false
|
albumMap[music.album] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is AlbumImpl -> {
|
is AlbumImpl -> {
|
||||||
music.link(this)
|
music.link(this)
|
||||||
albumMap[music] = true
|
albumMap[music] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> error("Unexpected input music $music in $name ${music::class.simpleName}")
|
else -> error("Unexpected input music $music in $name ${music::class.simpleName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -500,24 +234,13 @@ class ArtistImpl(
|
||||||
// we just compare raw instances and how they are interpreted.
|
// we just compare raw instances and how they are interpreted.
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
other is ArtistImpl &&
|
other is ArtistImpl &&
|
||||||
uid == other.uid &&
|
uid == other.uid &&
|
||||||
rawArtist == other.rawArtist &&
|
rawArtist == other.rawArtist &&
|
||||||
nameFactory == other.nameFactory &&
|
nameFactory == other.nameFactory &&
|
||||||
songs == other.songs
|
songs == other.songs
|
||||||
|
|
||||||
override fun toString() = "Artist(uid=$uid, name=$name)"
|
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.
|
* Perform final validation and organization on this instance.
|
||||||
|
@ -593,25 +316,13 @@ class GenreImpl(
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
other is GenreImpl &&
|
other is GenreImpl &&
|
||||||
uid == other.uid &&
|
uid == other.uid &&
|
||||||
rawGenre == other.rawGenre &&
|
rawGenre == other.rawGenre &&
|
||||||
nameFactory == other.nameFactory &&
|
nameFactory == other.nameFactory &&
|
||||||
songs == other.songs
|
songs == other.songs
|
||||||
|
|
||||||
override fun toString() = "Genre(uid=$uid, name=$name)"
|
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.
|
* 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.
|
* 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
|
||||||
|
|
Loading…
Reference in a new issue