musikr.cover: refactor cover

Instead of using a weird sealed class, instead go for a
Cover/CoverCollection system instead that removes some implicit
design dependence in musikr.
This commit is contained in:
Alexander Capehart 2024-12-24 14:43:48 -05:00
parent a24d102a00
commit 7768d98632
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
15 changed files with 205 additions and 76 deletions

View file

@ -64,7 +64,7 @@ import org.oxycblt.musikr.Artist
import org.oxycblt.musikr.Genre import org.oxycblt.musikr.Genre
import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverCollection
/** /**
* Auxio's extension of [ImageView] that enables cover art loading and playing indicator and * Auxio's extension of [ImageView] that enables cover art loading and playing indicator and
@ -327,7 +327,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
*/ */
fun bind(album: Album) = fun bind(album: Album) =
bindImpl( bindImpl(
album.cover, album.covers,
context.getString(R.string.desc_album_cover, album.name), context.getString(R.string.desc_album_cover, album.name),
R.drawable.ic_album_24) R.drawable.ic_album_24)
@ -338,7 +338,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
*/ */
fun bind(artist: Artist) = fun bind(artist: Artist) =
bindImpl( bindImpl(
artist.cover, artist.covers,
context.getString(R.string.desc_artist_image, artist.name), context.getString(R.string.desc_artist_image, artist.name),
R.drawable.ic_artist_24) R.drawable.ic_artist_24)
@ -349,7 +349,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
*/ */
fun bind(genre: Genre) = fun bind(genre: Genre) =
bindImpl( bindImpl(
genre.cover, genre.covers,
context.getString(R.string.desc_genre_image, genre.name), context.getString(R.string.desc_genre_image, genre.name),
R.drawable.ic_genre_24) R.drawable.ic_genre_24)
@ -360,7 +360,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
*/ */
fun bind(playlist: Playlist) = fun bind(playlist: Playlist) =
bindImpl( bindImpl(
playlist.cover, playlist.covers,
context.getString(R.string.desc_playlist_image, playlist.name), context.getString(R.string.desc_playlist_image, playlist.name),
R.drawable.ic_playlist_24) R.drawable.ic_playlist_24)
@ -372,9 +372,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
* @param errorRes The resource of the error drawable to use if the cover cannot be loaded. * @param errorRes The resource of the error drawable to use if the cover cannot be loaded.
*/ */
fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) = fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) =
bindImpl(Cover.multi(songs), desc, errorRes) bindImpl(CoverCollection.from(songs.mapNotNull { it.cover }), desc, errorRes)
private fun bindImpl(cover: Cover?, desc: String, @DrawableRes errorRes: Int) { private fun bindImpl(cover: Any?, desc: String, @DrawableRes errorRes: Int) {
val request = val request =
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(cover) .data(cover)

View file

@ -36,14 +36,17 @@ class CoilModule {
@Provides @Provides
fun imageLoader( fun imageLoader(
@ApplicationContext context: Context, @ApplicationContext context: Context,
keyer: CoverKeyer, coverKeyer: CoverKeyer,
factory: CoverFetcher.Factory coverFactory: CoverFetcher.Factory,
coverCollectionKeyer: CoverCollectionKeyer,
coverCollectionFactory: CoverCollectionFetcher.Factory
) = ) =
ImageLoader.Builder(context) ImageLoader.Builder(context)
.components { .components {
// Add fetchers for Music components to make them usable with ImageRequest add(coverKeyer)
add(keyer) add(coverFactory)
add(factory) add(coverCollectionKeyer)
add(coverCollectionFactory)
} }
// Use our own crossfade with error drawable support // Use our own crossfade with error drawable support
.transitionFactory(ErrorCrossfadeTransitionFactory()) .transitionFactory(ErrorCrossfadeTransitionFactory())

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2021 Auxio Project * Copyright (c) 2024 Auxio Project
* Components.kt is part of Auxio. * CoverCollectionFetcher.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
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -31,7 +31,6 @@ import coil3.fetch.FetchResult
import coil3.fetch.Fetcher import coil3.fetch.Fetcher
import coil3.fetch.ImageFetchResult import coil3.fetch.ImageFetchResult
import coil3.fetch.SourceFetchResult import coil3.fetch.SourceFetchResult
import coil3.key.Keyer
import coil3.request.Options import coil3.request.Options
import coil3.size.Dimension import coil3.size.Dimension
import coil3.size.Size import coil3.size.Size
@ -39,36 +38,24 @@ import coil3.size.pxOrElse
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okio.FileSystem import okio.FileSystem
import okio.buffer import okio.buffer
import okio.source import okio.source
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverCollection
class CoverKeyer @Inject constructor() : Keyer<Cover> { class CoverCollectionFetcher
override fun key(data: Cover, options: Options) = "${data.id}&${options.size}"
}
class CoverFetcher
private constructor( private constructor(
private val context: Context, private val context: Context,
private val cover: Cover, private val covers: CoverCollection,
private val size: Size, private val size: Size,
) : Fetcher { ) : Fetcher {
override suspend fun fetch(): FetchResult? { override suspend fun fetch(): FetchResult? {
val streams = val streams = covers.covers.asFlow().mapNotNull { it.open() }.take(4).toList()
when (val cover = cover) {
is Cover.Single -> listOfNotNull(cover.open())
is Cover.Multi ->
buildList {
for (single in cover.all) {
single.open()?.let { add(it) }
if (size == 4) {
break
}
}
}
}
// We don't immediately check for mosaic feasibility from album count alone, as that // We don't immediately check for mosaic feasibility from album count alone, as that
// does not factor in InputStreams failing to load. Instead, only check once we // does not factor in InputStreams failing to load. Instead, only check once we
// definitely have image data to use. // definitely have image data to use.
@ -79,6 +66,7 @@ private constructor(
withContext(Dispatchers.IO) { streams.forEach(InputStream::close) } withContext(Dispatchers.IO) { streams.forEach(InputStream::close) }
} }
} }
// Not enough covers for a mosaic, take the first one (if that even exists) // Not enough covers for a mosaic, take the first one (if that even exists)
val first = streams.firstOrNull() ?: return null val first = streams.firstOrNull() ?: return null
@ -146,8 +134,8 @@ private constructor(
return if (size.mod(2) > 0) size + 1 else size return if (size.mod(2) > 0) size + 1 else size
} }
class Factory @Inject constructor() : Fetcher.Factory<Cover> { class Factory @Inject constructor() : Fetcher.Factory<CoverCollection> {
override fun create(data: Cover, options: Options, imageLoader: ImageLoader) = override fun create(data: CoverCollection, options: Options, imageLoader: ImageLoader) =
CoverFetcher(options.context, data, options.size) CoverCollectionFetcher(options.context, data, options.size)
} }
} }

View file

@ -0,0 +1,110 @@
/*
* Copyright (c) 2024 Auxio Project
* CoverFetcher.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.image.coil
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import androidx.core.graphics.drawable.toDrawable
import coil3.ImageLoader
import coil3.asImage
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.ImageFetchResult
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.size.Dimension
import coil3.size.Size
import coil3.size.pxOrElse
import java.io.InputStream
import javax.inject.Inject
import okio.FileSystem
import okio.buffer
import okio.source
import org.oxycblt.musikr.cover.Cover
class CoverFetcher private constructor(private val context: Context, private val cover: Cover) :
Fetcher {
override suspend fun fetch(): FetchResult? {
val stream = cover.open() ?: return null
return SourceFetchResult(
source = ImageSource(stream.source().buffer(), FileSystem.SYSTEM, null),
mimeType = null,
dataSource = DataSource.DISK)
}
/** Derived from phonograph: https://github.com/kabouzeid/Phonograph */
private suspend fun createMosaic(streams: List<InputStream>, size: Size): FetchResult {
// Use whatever size coil gives us to create the mosaic.
val mosaicSize = android.util.Size(size.width.mosaicSize(), size.height.mosaicSize())
val mosaicFrameSize =
Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2))
val mosaicBitmap =
Bitmap.createBitmap(mosaicSize.width, mosaicSize.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(mosaicBitmap)
var x = 0
var y = 0
// For each stream, create a bitmap scaled to 1/4th of the mosaics combined size
// and place it on a corner of the canvas.
for (stream in streams) {
if (y == mosaicSize.height) {
break
}
// Crop the bitmap down to a square so it leaves no empty space
// TODO: Work around this
val bitmap =
SquareCropTransformation.INSTANCE.transform(
BitmapFactory.decodeStream(stream), mosaicFrameSize)
canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null)
x += bitmap.width
if (x == mosaicSize.width) {
x = 0
y += bitmap.height
}
}
// It's way easier to map this into a drawable then try to serialize it into an
// BufferedSource. Just make sure we mark it as "sampled" so Coil doesn't try to
// load low-res mosaics into high-res ImageViews.
return ImageFetchResult(
image = mosaicBitmap.toDrawable(context.resources).asImage(),
isSampled = true,
dataSource = DataSource.DISK)
}
private fun Dimension.mosaicSize(): Int {
// Since we want the mosaic to be perfectly divisible into two, we need to round any
// odd image sizes upwards to prevent the mosaic creation from failing.
val size = pxOrElse { 512 }
return if (size.mod(2) > 0) size + 1 else size
}
class Factory @Inject constructor() : Fetcher.Factory<Cover> {
override fun create(data: Cover, options: Options, imageLoader: ImageLoader) =
CoverFetcher(options.context, data)
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2021 Auxio Project
* Keyers.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.image.coil
import coil3.key.Keyer
import coil3.request.Options
import javax.inject.Inject
import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverCollection
class CoverKeyer @Inject constructor() : Keyer<Cover> {
override fun key(data: Cover, options: Options) = "${data.id}&${options.size}"
}
class CoverCollectionKeyer @Inject constructor() : Keyer<CoverCollection> {
override fun key(data: CoverCollection, options: Options) =
"multi:${data.hashCode()}&${options.size}"
}

View file

@ -70,7 +70,7 @@ class MutableRevisionedStoredCovers(context: Context, private val revision: UUID
} }
} }
class RevisionedCover(private val revision: UUID, val inner: Cover.Single) : Cover.Single by inner { class RevisionedCover(private val revision: UUID, val inner: Cover) : Cover by inner {
override val id: String override val id: String
get() = "${inner.id}@${revision}" get() = "${inner.id}@${revision}"
} }

View file

@ -26,6 +26,7 @@ import java.util.UUID
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverCollection
import org.oxycblt.musikr.fs.Format import org.oxycblt.musikr.fs.Format
import org.oxycblt.musikr.fs.Path import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Date
@ -276,7 +277,7 @@ interface Song : Music {
/** The date the audio file was added to the device, as a unix epoch timestamp. */ /** The date the audio file was added to the device, as a unix epoch timestamp. */
val dateAdded: Long val dateAdded: Long
/** Useful information to quickly obtain the album cover. */ /** Useful information to quickly obtain the album cover. */
val cover: Cover.Single? val cover: Cover?
/** /**
* The parent [Album]. If the metadata did not specify an album, it's parent directory is used * The parent [Album]. If the metadata did not specify an album, it's parent directory is used
* instead. * instead.
@ -310,7 +311,7 @@ interface Album : MusicParent {
*/ */
val releaseType: ReleaseType val releaseType: ReleaseType
/** Cover information from album's songs. */ /** Cover information from album's songs. */
val cover: Cover val covers: CoverCollection
/** The duration of all songs in the album, in milliseconds. */ /** The duration of all songs in the album, in milliseconds. */
val durationMs: Long val durationMs: Long
/** The earliest date a song in this album was added, as a unix epoch timestamp. */ /** The earliest date a song in this album was added, as a unix epoch timestamp. */
@ -340,7 +341,7 @@ interface Artist : MusicParent {
*/ */
val durationMs: Long? val durationMs: Long?
/** Useful information to quickly obtain a (single) cover for a Genre. */ /** Useful information to quickly obtain a (single) cover for a Genre. */
val cover: Cover val covers: CoverCollection
/** The [Genre]s of this artist. */ /** The [Genre]s of this artist. */
val genres: List<Genre> val genres: List<Genre>
} }
@ -356,7 +357,7 @@ interface Genre : MusicParent {
/** The total duration of the songs in this genre, in milliseconds. */ /** The total duration of the songs in this genre, in milliseconds. */
val durationMs: Long val durationMs: Long
/** Useful information to quickly obtain a (single) cover for a Genre. */ /** Useful information to quickly obtain a (single) cover for a Genre. */
val cover: Cover val covers: CoverCollection
} }
/** /**
@ -370,5 +371,5 @@ interface Playlist : MusicParent {
/** The total duration of the songs in this genre, in milliseconds. */ /** The total duration of the songs in this genre, in milliseconds. */
val durationMs: Long val durationMs: Long
/** Useful information to quickly obtain a (single) cover for a Genre. */ /** Useful information to quickly obtain a (single) cover for a Genre. */
val cover: Cover val covers: CoverCollection
} }

View file

@ -19,29 +19,22 @@
package org.oxycblt.musikr.cover package org.oxycblt.musikr.cover
import java.io.InputStream import java.io.InputStream
import org.oxycblt.musikr.Song
sealed interface Cover { interface Cover {
val id: String val id: String
interface Single : Cover { suspend fun open(): InputStream?
suspend fun open(): InputStream? }
}
class Multi(val all: List<Single>) : Cover {
override val id = "multi@${all.hashCode()}"
}
class CoverCollection private constructor(val covers: List<Cover>) {
companion object { companion object {
fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) } fun from(covers: Collection<Cover>) =
CoverCollection(
private fun order(songs: Collection<Song>) = covers
songs .groupBy { it.id }
.mapNotNull { it.cover } .entries
.groupBy { it.id } .sortedByDescending { it.key }
.entries .sortedByDescending { it.value.size }
.sortedByDescending { it.key } .map { it.value.first() })
.sortedByDescending { it.value.size }
.map { it.value.first() }
} }
} }

View file

@ -21,7 +21,7 @@ package org.oxycblt.musikr.cover
import android.content.Context import android.content.Context
interface StoredCovers { interface StoredCovers {
suspend fun obtain(id: String): Cover.Single? suspend fun obtain(id: String): Cover?
companion object { companion object {
fun from(context: Context, path: String, format: CoverFormat): MutableStoredCovers = fun from(context: Context, path: String, format: CoverFormat): MutableStoredCovers =
@ -30,7 +30,7 @@ interface StoredCovers {
} }
interface MutableStoredCovers : StoredCovers { interface MutableStoredCovers : StoredCovers {
suspend fun write(data: ByteArray): Cover.Single? suspend fun write(data: ByteArray): Cover?
} }
private class FileStoredCovers( private class FileStoredCovers(
@ -45,6 +45,6 @@ private class FileStoredCovers(
} }
} }
private class FileCover(override val id: String, private val coverFile: CoverFile) : Cover.Single { private class FileCover(override val id: String, private val coverFile: CoverFile) : Cover {
override suspend fun open() = coverFile.open() override suspend fun open() = coverFile.open()
} }

View file

@ -22,7 +22,7 @@ import org.oxycblt.musikr.Album
import org.oxycblt.musikr.Artist import org.oxycblt.musikr.Artist
import org.oxycblt.musikr.Music import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverCollection
import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.interpret.PreAlbum import org.oxycblt.musikr.tag.interpret.PreAlbum
import org.oxycblt.musikr.util.update import org.oxycblt.musikr.util.update
@ -56,7 +56,7 @@ internal class AlbumImpl(private val core: AlbumCore) : Album {
override val releaseType = preAlbum.releaseType override val releaseType = preAlbum.releaseType
override val durationMs = core.songs.sumOf { it.durationMs } override val durationMs = core.songs.sumOf { it.durationMs }
override val dateAdded = core.songs.minOf { it.dateAdded } override val dateAdded = core.songs.minOf { it.dateAdded }
override val cover = Cover.multi(core.songs) override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover })
override val dates: Date.Range? = override val dates: Date.Range? =
core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) } core.songs.mapNotNull { it.date }.ifEmpty { null }?.run { Date.Range(min(), max()) }

View file

@ -23,7 +23,7 @@ import org.oxycblt.musikr.Artist
import org.oxycblt.musikr.Genre import org.oxycblt.musikr.Genre
import org.oxycblt.musikr.Music import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverCollection
import org.oxycblt.musikr.tag.interpret.PreArtist import org.oxycblt.musikr.tag.interpret.PreArtist
import org.oxycblt.musikr.util.update import org.oxycblt.musikr.util.update
@ -55,7 +55,7 @@ internal class ArtistImpl(private val core: ArtistCore) : Artist {
get() = core.resolveGenres().toList() get() = core.resolveGenres().toList()
override val durationMs = core.songs.sumOf { it.durationMs } override val durationMs = core.songs.sumOf { it.durationMs }
override val cover = Cover.multi(core.songs) override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover })
private val hashCode = private val hashCode =
31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode() 31 * (31 * uid.hashCode() + core.preArtist.hashCode()) * core.songs.hashCode()

View file

@ -22,7 +22,7 @@ import org.oxycblt.musikr.Artist
import org.oxycblt.musikr.Genre import org.oxycblt.musikr.Genre
import org.oxycblt.musikr.Music import org.oxycblt.musikr.Music
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverCollection
import org.oxycblt.musikr.tag.interpret.PreGenre import org.oxycblt.musikr.tag.interpret.PreGenre
import org.oxycblt.musikr.util.update import org.oxycblt.musikr.util.update
@ -44,7 +44,7 @@ internal class GenreImpl(private val core: GenreCore) : Genre {
override val songs = core.songs override val songs = core.songs
override val artists = core.artists override val artists = core.artists
override val durationMs = core.songs.sumOf { it.durationMs } override val durationMs = core.songs.sumOf { it.durationMs }
override val cover = Cover.multi(core.songs) override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover })
private val hashCode = 31 * (31 * uid.hashCode() + core.preGenre.hashCode()) + songs.hashCode() private val hashCode = 31 * (31 * uid.hashCode() + core.preGenre.hashCode()) + songs.hashCode()

View file

@ -20,7 +20,7 @@ package org.oxycblt.musikr.model
import org.oxycblt.musikr.Playlist import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.Song import org.oxycblt.musikr.Song
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.CoverCollection
import org.oxycblt.musikr.playlist.interpret.PrePlaylistInfo import org.oxycblt.musikr.playlist.interpret.PrePlaylistInfo
import org.oxycblt.musikr.tag.Name import org.oxycblt.musikr.tag.Name
@ -33,7 +33,7 @@ internal class PlaylistImpl(val core: PlaylistCore) : Playlist {
override val uid = core.prePlaylist.handle.uid override val uid = core.prePlaylist.handle.uid
override val name: Name.Known = core.prePlaylist.name override val name: Name.Known = core.prePlaylist.name
override val durationMs = core.songs.sumOf { it.durationMs } override val durationMs = core.songs.sumOf { it.durationMs }
override val cover = Cover.multi(core.songs) override val covers = CoverCollection.from(core.songs.mapNotNull { it.cover })
override val songs = core.songs override val songs = core.songs
private var hashCode = private var hashCode =

View file

@ -163,7 +163,7 @@ internal data class RawSong(
val file: DeviceFile, val file: DeviceFile,
val properties: Properties, val properties: Properties,
val tags: ParsedTags, val tags: ParsedTags,
val cover: Cover.Single? val cover: Cover?
) )
internal sealed interface ExtractedMusic { internal sealed interface ExtractedMusic {

View file

@ -48,7 +48,7 @@ internal data class PreSong(
val replayGainAdjustment: ReplayGainAdjustment, val replayGainAdjustment: ReplayGainAdjustment,
val lastModified: Long, val lastModified: Long,
val dateAdded: Long, val dateAdded: Long,
val cover: Cover.Single?, val cover: Cover?,
val preAlbum: PreAlbum, val preAlbum: PreAlbum,
val preArtists: List<PreArtist>, val preArtists: List<PreArtist>,
val preGenres: List<PreGenre> val preGenres: List<PreGenre>