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:
parent
a24d102a00
commit
7768d98632
15 changed files with 205 additions and 76 deletions
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
110
app/src/main/java/org/oxycblt/auxio/image/coil/CoverFetcher.kt
Normal file
110
app/src/main/java/org/oxycblt/auxio/image/coil/CoverFetcher.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
34
app/src/main/java/org/oxycblt/auxio/image/coil/Keyers.kt
Normal file
34
app/src/main/java/org/oxycblt/auxio/image/coil/Keyers.kt
Normal 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}"
|
||||||
|
}
|
|
@ -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}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()) }
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue