in-progress interpreter refactor

Will force-rewrite at several points.
This commit is contained in:
Alexander Capehart 2024-11-09 20:06:53 -07:00
parent 85bd1f0062
commit a784f73c5e
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 408 additions and 368 deletions

View file

@ -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
}

View file

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

View file

@ -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
}

View file

@ -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.
*

View file

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

View 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?,
)

View file

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

View file

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