Compare commits

...

1 commit
dev ... music2

Author SHA1 Message Date
Alexander Capehart
a784f73c5e
in-progress interpreter refactor
Will force-rewrite at several points.
2024-11-09 20:06:53 -07:00
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>, 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()
} }

View file

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

View file

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

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