diff --git a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt index 148ee3da3..39df3369c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/CoverView.kt @@ -316,7 +316,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr */ fun bind(song: Song) = bindImpl( - listOf(song.cover), + song.cover, context.getString(R.string.desc_album_cover, song.album.name), R.drawable.ic_album_24) @@ -327,7 +327,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr */ fun bind(album: Album) = bindImpl( - album.cover.all, + album.cover, context.getString(R.string.desc_album_cover, album.name), R.drawable.ic_album_24) @@ -338,7 +338,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr */ fun bind(artist: Artist) = bindImpl( - artist.cover.all, + artist.cover, context.getString(R.string.desc_artist_image, artist.name), R.drawable.ic_artist_24) @@ -349,7 +349,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr */ fun bind(genre: Genre) = bindImpl( - genre.cover.all, + genre.cover, context.getString(R.string.desc_genre_image, genre.name), R.drawable.ic_genre_24) @@ -360,7 +360,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr */ fun bind(playlist: Playlist) = bindImpl( - playlist.cover?.all ?: emptyList(), + playlist.cover ?: Cover.nil(), context.getString(R.string.desc_playlist_image, playlist.name), R.drawable.ic_playlist_24) @@ -372,12 +372,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * @param errorRes The resource of the error drawable to use if the cover cannot be loaded. */ fun bind(songs: List, desc: String, @DrawableRes errorRes: Int) = - bindImpl(Cover.order(songs), desc, errorRes) + bindImpl(Cover.multi(songs), desc, errorRes) - private fun bindImpl(covers: List, desc: String, @DrawableRes errorRes: Int) { + private fun bindImpl(cover: Cover, desc: String, @DrawableRes errorRes: Int) { val request = ImageRequest.Builder(context) - .data(covers) + .data(cover) .error( StyledDrawable(context, context.getDrawableCompat(errorRes), iconSize) .asImage()) diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt index bd1551f8a..a095171d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt @@ -18,29 +18,138 @@ package org.oxycblt.auxio.image.extractor +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.key.Keyer 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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okio.FileSystem +import okio.buffer +import okio.source +import org.oxycblt.auxio.image.stack.extractor.CoverExtractor -class CoverKeyer @Inject constructor() : Keyer> { - override fun key(data: Collection, options: Options) = - "${data.map { it.key }.hashCode()}" +class CoverKeyer @Inject constructor() : Keyer { + override fun key(data: Cover, options: Options) = "${data.key}&${options.size}" } class CoverFetcher private constructor( - private val covers: Collection, + private val context: Context, + private val cover: Cover, private val size: Size, private val coverExtractor: CoverExtractor, ) : Fetcher { - override suspend fun fetch() = coverExtractor.extract(covers, size) + override suspend fun fetch(): FetchResult? { + val streams = + when (val cover = cover) { + is Cover.Single -> listOfNotNull(coverExtractor.extract(cover)) + is Cover.Multi -> + buildList { + for (single in cover.all) { + coverExtractor.extract(single)?.let { add(it) } + if (size == 4) { + break + } + } + } + } + // 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 + // definitely have image data to use. + if (streams.size == 4) { + // Make sure we free the InputStreams once we've transformed them into a mosaic. + return createMosaic(streams, size).also { + withContext(Dispatchers.IO) { streams.forEach(InputStream::close) } + } + } + + // Not enough covers for a mosaic, take the first one (if that even exists) + val first = streams.firstOrNull() ?: return null + + // All but the first stream will be unused, free their resources + withContext(Dispatchers.IO) { + for (i in 1 until streams.size) { + streams[i].close() + } + } + + return SourceFetchResult( + source = ImageSource(first.source().buffer(), FileSystem.SYSTEM, null), + mimeType = null, + dataSource = DataSource.DISK) + } + + /** Derived from phonograph: https://github.com/kabouzeid/Phonograph */ + private suspend fun createMosaic(streams: List, 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(private val coverExtractor: CoverExtractor) : - Fetcher.Factory> { - override fun create(data: Collection, options: Options, imageLoader: ImageLoader) = - CoverFetcher(data, options.size, coverExtractor) + Fetcher.Factory { + override fun create(data: Cover, options: Options, imageLoader: ImageLoader) = + CoverFetcher(options.context, data, options.size, coverExtractor) } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Cover.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Cover.kt index bf3cf97cf..14bfca6df 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Cover.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Cover.kt @@ -18,49 +18,35 @@ package org.oxycblt.auxio.image.extractor -import android.net.Uri import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Song sealed interface Cover { val key: String - val mediaStoreCoverUri: Uri - /** - * The song has an embedded cover art we support, so we can operate with it on a per-song basis. - */ - data class Embedded(val songCoverUri: Uri, val songUri: Uri, val perceptualHash: String) : - Cover { - override val mediaStoreCoverUri = songCoverUri - override val key = perceptualHash + class Single(song: Song) : Cover { + override val key = "${song.uid}@${song.lastModified}" + val uri = song.uri } - /** - * We couldn't find any embedded cover art ourselves, but the android system might have some - * through a cover.jpg file or something similar. - */ - data class External(val albumCoverUri: Uri) : Cover { - override val mediaStoreCoverUri = albumCoverUri - override val key = albumCoverUri.toString() + class Multi(val all: List) : Cover { + override val key = "multi@${all.map { it.key }.hashCode()}" } companion object { private val FALLBACK_SORT = Sort(Sort.Mode.ByAlbum, Sort.Direction.ASCENDING) - fun order(songs: Collection) = + fun nil() = Multi(listOf()) + + fun single(song: Song) = Single(song) + + fun multi(songs: Collection) = order(songs).run { Multi(this) } + + private fun order(songs: Collection) = FALLBACK_SORT.songs(songs) - .map { it.cover } - .groupBy { it.key } + .groupBy { it.album } .entries .sortedByDescending { it.value.size } - .map { it.value.first() } - } -} - -data class ParentCover(val single: Cover, val all: List) { - companion object { - fun from(song: Song, songs: Collection) = from(song.cover, songs) - - fun from(src: Cover, songs: Collection) = ParentCover(src, Cover.order(songs)) + .map { it.value.first().cover } } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt deleted file mode 100644 index 1f309a2df..000000000 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/CoverExtractor.kt +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (c) 2023 Auxio Project - * CoverExtractor.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 . - */ - -package org.oxycblt.auxio.image.extractor - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.media.MediaMetadataRetriever -import android.util.Size as AndroidSize -import androidx.core.graphics.drawable.toDrawable -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import androidx.media3.common.Metadata -import androidx.media3.exoplayer.MetadataRetriever -import androidx.media3.exoplayer.source.MediaSource -import androidx.media3.extractor.metadata.flac.PictureFrame -import androidx.media3.extractor.metadata.id3.ApicFrame -import coil3.asImage -import coil3.decode.DataSource -import coil3.decode.ImageSource -import coil3.fetch.FetchResult -import coil3.fetch.ImageFetchResult -import coil3.fetch.SourceFetchResult -import coil3.size.Dimension -import coil3.size.Size -import coil3.size.pxOrElse -import dagger.hilt.android.qualifiers.ApplicationContext -import java.io.ByteArrayInputStream -import java.io.InputStream -import javax.inject.Inject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.guava.asDeferred -import kotlinx.coroutines.withContext -import okio.FileSystem -import okio.buffer -import okio.source -import org.oxycblt.auxio.image.CoverMode -import org.oxycblt.auxio.image.ImageSettings -import org.oxycblt.auxio.music.Song -import timber.log.Timber as L - -/** - * Provides functionality for extracting album cover information. Meant for internal use only. - * - * @author Alexander Capehart (OxygenCobalt) - */ -class CoverExtractor -@Inject -constructor( - @ApplicationContext private val context: Context, - private val imageSettings: ImageSettings, - private val mediaSourceFactory: MediaSource.Factory -) { - /** - * Extract an image (in the form of [FetchResult]) to represent the given [Song]s. - * - * @param covers The [Cover]s to load. - * @param size The [Size] of the image to load. - * @return If four distinct album covers could be extracted from the [Song]s, a [DrawableResult] - * will be returned of a mosaic composed of the first four loaded album covers. Otherwise, a - * [SourceResult] of one album cover will be returned. - */ - suspend fun extract(covers: Collection, size: Size): FetchResult? { - val streams = mutableListOf() - for (cover in covers) { - openCoverInputStream(cover)?.let(streams::add) - // 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 - // definitely have image data to use. - if (streams.size == 4) { - // Make sure we free the InputStreams once we've transformed them into a mosaic. - return createMosaic(streams, size).also { - withContext(Dispatchers.IO) { streams.forEach(InputStream::close) } - } - } - } - - // Not enough covers for a mosaic, take the first one (if that even exists) - val first = streams.firstOrNull() ?: return null - - // All but the first stream will be unused, free their resources - withContext(Dispatchers.IO) { - for (i in 1 until streams.size) { - streams[i].close() - } - } - - return SourceFetchResult( - source = ImageSource(first.source().buffer(), FileSystem.SYSTEM, null), - mimeType = null, - dataSource = DataSource.DISK) - } - - fun findCoverDataInMetadata(metadata: Metadata): InputStream? { - var stream: ByteArrayInputStream? = null - - for (i in 0 until metadata.length()) { - // We can only extract pictures from two tags with this method, ID3v2's APIC or - // Vorbis picture comments. - val pic: ByteArray? - val type: Int - - when (val entry = metadata.get(i)) { - is ApicFrame -> { - pic = entry.pictureData - type = entry.pictureType - } - is PictureFrame -> { - pic = entry.pictureData - type = entry.pictureType - } - else -> continue - } - - if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) { - stream = ByteArrayInputStream(pic) - break - } else if (stream == null) { - stream = ByteArrayInputStream(pic) - } - } - - return stream - } - - private suspend fun openCoverInputStream(cover: Cover) = - try { - when (cover) { - is Cover.Embedded -> - when (imageSettings.coverMode) { - CoverMode.OFF -> null - CoverMode.MEDIA_STORE -> extractMediaStoreCover(cover) - CoverMode.QUALITY -> extractQualityCover(cover) - } - is Cover.External -> { - extractMediaStoreCover(cover) - } - } - } catch (e: Exception) { - L.e("Unable to extract album cover due to an error: $e") - null - } - - private suspend fun extractQualityCover(cover: Cover.Embedded) = - extractExoplayerCover(cover) - ?: extractAospMetadataCover(cover) - ?: extractMediaStoreCover(cover) - - private fun extractAospMetadataCover(cover: Cover.Embedded): InputStream? = - MediaMetadataRetriever().run { - // This call is time-consuming but it also doesn't seem to hold up the main thread, - // so it's probably fine not to wrap it.rmt - setDataSource(context, cover.songUri) - - // Get the embedded picture from MediaMetadataRetriever, which will return a full - // ByteArray of the cover without any compression artifacts. - // If its null [i.e there is no embedded cover], than just ignore it and move on - embeddedPicture?.let { ByteArrayInputStream(it) }.also { release() } - } - - private suspend fun extractExoplayerCover(cover: Cover.Embedded): InputStream? { - val tracks = - MetadataRetriever.retrieveMetadata(mediaSourceFactory, MediaItem.fromUri(cover.songUri)) - .asDeferred() - .await() - - // The metadata extraction process of ExoPlayer results in a dump of all metadata - // it found, which must be iterated through. - val metadata = tracks[0].getFormat(0).metadata - - if (metadata == null || metadata.length() == 0) { - // No (parsable) metadata. This is also expected. - return null - } - - return findCoverDataInMetadata(metadata) - } - - @SuppressLint("Recycle") - private suspend fun extractMediaStoreCover(cover: Cover) = - // Eliminate any chance that this blocking call might mess up the loading process - withContext(Dispatchers.IO) { - // Coil will recycle this InputStream, so we don't need to worry about it. - context.contentResolver.openInputStream(cover.mediaStoreCoverUri) - } - - /** Derived from phonograph: https://github.com/kabouzeid/Phonograph */ - private suspend fun createMosaic(streams: List, size: Size): FetchResult { - // Use whatever size coil gives us to create the mosaic. - val mosaicSize = AndroidSize(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 - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/DHash.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/DHash.kt deleted file mode 100644 index 1e7809606..000000000 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/DHash.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2024 Auxio Project - * DHash.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 . - */ - -package org.oxycblt.auxio.image.extractor - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.ColorMatrix -import android.graphics.ColorMatrixColorFilter -import android.graphics.Paint -import java.math.BigInteger - -@Suppress("UNUSED") -fun Bitmap.dHash(hashSize: Int = 16): String { - // Step 1: Resize the bitmap to a fixed size - val resizedBitmap = Bitmap.createScaledBitmap(this, hashSize + 1, hashSize, true) - - // Step 2: Convert the bitmap to grayscale - val grayBitmap = - Bitmap.createBitmap(resizedBitmap.width, resizedBitmap.height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(grayBitmap) - val paint = Paint() - val colorMatrix = ColorMatrix() - colorMatrix.setSaturation(0f) - val filter = ColorMatrixColorFilter(colorMatrix) - paint.colorFilter = filter - canvas.drawBitmap(resizedBitmap, 0f, 0f, paint) - - // Step 3: Compute the difference between adjacent pixels - var hash = BigInteger.valueOf(0) - val one = BigInteger.valueOf(1) - for (y in 0 until hashSize) { - for (x in 0 until hashSize) { - val pixel1 = grayBitmap.getPixel(x, y) - val pixel2 = grayBitmap.getPixel(x + 1, y) - val diff = Color.red(pixel1) - Color.red(pixel2) - if (diff > 0) { - hash = hash.or(one.shl(y * hashSize + x)) - } - } - } - - return hash.toString(16) -} diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/CoverRetriever.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/CoverRetriever.kt new file mode 100644 index 000000000..14e0f512d --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/CoverRetriever.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 Auxio Project + * CoverRetriever.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 . + */ + +package org.oxycblt.auxio.image.stack + +import java.io.InputStream +import javax.inject.Inject +import org.oxycblt.auxio.image.stack.cache.CoverCache +import org.oxycblt.auxio.music.Song + +interface CoverRetriever { + suspend fun retrieve(song: Song): InputStream? +} + +class CoverRetrieverImpl +@Inject +constructor(private val coverCache: CoverCache, private val coverRetriever: CoverRetriever) : + CoverRetriever { + override suspend fun retrieve(song: Song) = + coverCache.read(song) ?: coverRetriever.retrieve(song)?.also { coverCache.write(song, it) } +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/StackModule.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/StackModule.kt new file mode 100644 index 000000000..4fc3c5eb8 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/StackModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Auxio Project + * StackModule.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 . + */ + +package org.oxycblt.auxio.image.stack + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface StackModule { + @Binds fun coverRetriever(impl: CoverRetrieverImpl): CoverRetriever +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/AppFiles.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/AppFiles.kt index 6e2a34309..fb5caf54c 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/AppFiles.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/AppFiles.kt @@ -1,21 +1,39 @@ +/* + * Copyright (c) 2024 Auxio Project + * AppFiles.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 . + */ + package org.oxycblt.auxio.image.stack.cache import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import java.io.IOException import java.io.InputStream import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext interface AppFiles { suspend fun read(file: String): InputStream? + suspend fun write(file: String, inputStream: InputStream): Boolean } -class AppFilesImpl @Inject constructor( - @ApplicationContext private val context: Context -) : AppFiles { +class AppFilesImpl @Inject constructor(@ApplicationContext private val context: Context) : + AppFiles { override suspend fun read(file: String): InputStream? = withContext(context = Dispatchers.IO) { try { @@ -38,5 +56,4 @@ class AppFilesImpl @Inject constructor( inputStream.close() } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CacheModule.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CacheModule.kt index 27dcb610d..ae54a32f8 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CacheModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CacheModule.kt @@ -1,7 +1,24 @@ +/* + * Copyright (c) 2024 Auxio Project + * CacheModule.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 . + */ + package org.oxycblt.auxio.image.stack.cache import android.content.Context -import androidx.media3.datasource.cache.Cache import androidx.room.Room import dagger.Binds import dagger.Module @@ -9,9 +26,6 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import org.oxycblt.auxio.music.stack.explore.cache.TagCache -import org.oxycblt.auxio.music.stack.explore.cache.TagCacheImpl -import org.oxycblt.auxio.music.stack.explore.cache.TagDatabase import javax.inject.Singleton @Module @@ -19,8 +33,6 @@ import javax.inject.Singleton interface StackModule { @Binds fun appFiles(impl: AppFilesImpl): AppFiles - @Binds fun cache(impl: CoverCacheImpl): Cache - @Binds fun perceptualHash(perceptualHash: PerceptualHashImpl): PerceptualHash @Binds fun coverCache(cache: CoverCacheImpl): CoverCache @@ -32,7 +44,8 @@ class StoredCoversDatabaseModule { @Singleton @Provides fun database(@ApplicationContext context: Context) = - Room.databaseBuilder(context.applicationContext, StoredCoversDatabase::class.java, "stored_covers.db") + Room.databaseBuilder( + context.applicationContext, StoredCoversDatabase::class.java, "stored_covers.db") .fallbackToDestructiveMigration() .build() } diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CoverCache.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CoverCache.kt index 1e211f022..ae6ea3101 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CoverCache.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CoverCache.kt @@ -1,68 +1,89 @@ +/* + * Copyright (c) 2024 Auxio Project + * CoverCache.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 . + */ + package org.oxycblt.auxio.image.stack.cache import android.graphics.Bitmap import android.graphics.BitmapFactory import android.os.Build -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.oxycblt.auxio.music.Song import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStream import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.oxycblt.auxio.music.Song interface CoverCache { suspend fun read(song: Song): InputStream? + suspend fun write(song: Song, inputStream: InputStream): Boolean } - -class CoverCacheImpl @Inject constructor( +class CoverCacheImpl +@Inject +constructor( private val storedCoversDao: StoredCoversDao, private val appFiles: AppFiles, private val perceptualHash: PerceptualHash ) : CoverCache { override suspend fun read(song: Song): InputStream? { - val perceptualHash = storedCoversDao.getCoverFile(song.uid, song.lastModified) - ?: return null + val perceptualHash = + storedCoversDao.getCoverFile(song.uid, song.lastModified) ?: return null return appFiles.read(fileName(perceptualHash)) } - override suspend fun write(song: Song, inputStream: InputStream): Boolean = withContext(Dispatchers.IO) { - val bitmap = BitmapFactory.decodeStream(inputStream) - val perceptualHash = perceptualHash.hash(bitmap) + override suspend fun write(song: Song, inputStream: InputStream): Boolean = + withContext(Dispatchers.IO) { + val bitmap = BitmapFactory.decodeStream(inputStream) + val perceptualHash = perceptualHash.hash(bitmap) - // Compress bitmap down to webp into another inputstream - val compressedStream = ByteArrayOutputStream().use { outputStream -> - bitmap.compress(COVER_CACHE_FORMAT, 80, outputStream) - ByteArrayInputStream(outputStream.toByteArray()) + // Compress bitmap down to webp into another inputstream + val compressedStream = + ByteArrayOutputStream().use { outputStream -> + bitmap.compress(COVER_CACHE_FORMAT, 80, outputStream) + ByteArrayInputStream(outputStream.toByteArray()) + } + + val writeSuccess = appFiles.write(fileName(perceptualHash), compressedStream) + + if (writeSuccess) { + storedCoversDao.setCoverFile( + StoredCover( + uid = song.uid, + lastModified = song.lastModified, + perceptualHash = perceptualHash)) + } + + writeSuccess } - val writeSuccess = appFiles.write(fileName(perceptualHash), compressedStream) - - if (writeSuccess) { - storedCoversDao.setCoverFile( - StoredCover( - uid = song.uid, - lastModified = song.lastModified, - perceptualHash = perceptualHash - ) - ) - } - - writeSuccess - } - private fun fileName(perceptualHash: String) = "cover_$perceptualHash.png" private companion object { @Suppress("DEPRECATION") - val COVER_CACHE_FORMAT = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Bitmap.CompressFormat.WEBP_LOSSY - } else { - Bitmap.CompressFormat.WEBP - } + val COVER_CACHE_FORMAT = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Bitmap.CompressFormat.WEBP_LOSSY + } else { + Bitmap.CompressFormat.WEBP + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/PerceptualHash.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/PerceptualHash.kt index 21a21f4f8..7651d7324 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/PerceptualHash.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/PerceptualHash.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 Auxio Project + * PerceptualHash.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 . + */ + package org.oxycblt.auxio.image.stack.cache import android.graphics.Bitmap @@ -45,5 +63,4 @@ class PerceptualHashImpl : PerceptualHash { return hash.toString(16) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/StoredCoversDatabase.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/StoredCoversDatabase.kt index 29e88f143..f168cdf95 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/StoredCoversDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/StoredCoversDatabase.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2024 Auxio Project + * StoredCoversDatabase.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 . + */ + package org.oxycblt.auxio.image.stack.cache import androidx.room.Dao @@ -18,18 +36,18 @@ abstract class StoredCoversDatabase : RoomDatabase() { @Dao interface StoredCoversDao { - @Query("SELECT perceptualHash FROM StoredCover WHERE uid = :uid AND lastModified = :lastModified") + @Query( + "SELECT perceptualHash FROM StoredCover WHERE uid = :uid AND lastModified = :lastModified") + @TypeConverters(Music.UID.TypeConverters::class) fun getCoverFile(uid: Music.UID, lastModified: Long): String? - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun setCoverFile(storedCover: StoredCover) + @Insert(onConflict = OnConflictStrategy.REPLACE) fun setCoverFile(storedCover: StoredCover) } @Entity @TypeConverters(Music.UID.TypeConverters::class) data class StoredCover( - @PrimaryKey - val uid: Music.UID, + @PrimaryKey val uid: Music.UID, val lastModified: Long, val perceptualHash: String -) \ No newline at end of file +) diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/AOSPCoverSource.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/AOSPCoverSource.kt new file mode 100644 index 000000000..87fe72171 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/AOSPCoverSource.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Auxio Project + * AOSPCoverSource.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 . + */ + +package org.oxycblt.auxio.image.stack.extractor + +import android.media.MediaMetadataRetriever +import android.net.Uri +import java.io.InputStream +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AOSPCoverSource @Inject constructor() : CoverSource { + override suspend fun extract(fileUri: Uri): InputStream? { + val mediaMetadataRetriever = MediaMetadataRetriever() + val cover = + withContext(Dispatchers.IO) { + mediaMetadataRetriever.setDataSource(fileUri.toString()) + mediaMetadataRetriever.embeddedPicture + } ?: return null + return cover.inputStream() + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/CoverExtractor.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/CoverExtractor.kt new file mode 100644 index 000000000..795092b86 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/CoverExtractor.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Auxio Project + * CoverExtractor.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 . + */ + +package org.oxycblt.auxio.image.stack.extractor + +import android.net.Uri +import java.io.InputStream +import javax.inject.Inject +import org.oxycblt.auxio.image.extractor.Cover + +interface CoverExtractor { + suspend fun extract(cover: Cover.Single): InputStream? +} + +data class CoverSources(val sources: List) + +interface CoverSource { + suspend fun extract(fileUri: Uri): InputStream? +} + +class CoverExtractorImpl @Inject constructor(private val coverSources: CoverSources) : + CoverExtractor { + override suspend fun extract(cover: Cover.Single): InputStream? { + for (coverSource in coverSources.sources) { + val stream = coverSource.extract(cover.uri) + if (stream != null) { + return stream + } + } + return null + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/ExoPlayerCoverSource.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/ExoPlayerCoverSource.kt new file mode 100644 index 000000000..576d26a4a --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/ExoPlayerCoverSource.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 Auxio Project + * ExoPlayerCoverSource.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 . + */ + +package org.oxycblt.auxio.image.stack.extractor + +import android.net.Uri +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.Metadata +import androidx.media3.exoplayer.MetadataRetriever +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.extractor.metadata.flac.PictureFrame +import androidx.media3.extractor.metadata.id3.ApicFrame +import java.io.ByteArrayInputStream +import java.io.InputStream +import javax.inject.Inject +import kotlinx.coroutines.guava.asDeferred + +class ExoPlayerCoverSource +@Inject +constructor(private val mediaSourceFactory: MediaSource.Factory) : CoverSource { + override suspend fun extract(fileUri: Uri): InputStream? { + val tracks = + MetadataRetriever.retrieveMetadata(mediaSourceFactory, MediaItem.fromUri(fileUri)) + .asDeferred() + .await() + + // The metadata extraction process of ExoPlayer results in a dump of all metadata + // it found, which must be iterated through. + val metadata = tracks[0].getFormat(0).metadata + + if (metadata == null || metadata.length() == 0) { + // No (parsable) metadata. This is also expected. + return null + } + + return findCoverDataInMetadata(metadata) + } + + private fun findCoverDataInMetadata(metadata: Metadata): InputStream? { + var stream: ByteArrayInputStream? = null + + for (i in 0 until metadata.length()) { + // We can only extract pictures from two tags with this method, ID3v2's APIC or + // Vorbis picture comments. + val pic: ByteArray? + val type: Int + + when (val entry = metadata.get(i)) { + is ApicFrame -> { + pic = entry.pictureData + type = entry.pictureType + } + is PictureFrame -> { + pic = entry.pictureData + type = entry.pictureType + } + else -> continue + } + + if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) { + stream = ByteArrayInputStream(pic) + break + } else if (stream == null) { + stream = ByteArrayInputStream(pic) + } + } + + return stream + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/ExtractorModule.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/ExtractorModule.kt new file mode 100644 index 000000000..a74062e9f --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/stack/extractor/ExtractorModule.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 Auxio Project + * ExtractorModule.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 . + */ + +package org.oxycblt.auxio.image.stack.extractor + +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface ExtractorModule { + @Binds fun coverExtractor(impl: CoverExtractorImpl): CoverExtractor +} + +@Module +@InstallIn(SingletonComponent::class) +class CoverSourcesModule { + @Provides + fun coverSources(exoPlayerCoverSource: ExoPlayerCoverSource, aospCoverSource: AOSPCoverSource) = + CoverSources(listOf(exoPlayerCoverSource, aospCoverSource)) +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 239d928a0..3cf52cbfc 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -28,7 +28,6 @@ import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.image.extractor.Cover -import org.oxycblt.auxio.image.extractor.ParentCover import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Disc @@ -269,8 +268,6 @@ interface Song : Music { * audio file in a way that is scoped-storage-safe. */ val uri: Uri - /** Useful information to quickly obtain the album cover. */ - val cover: Cover /** * The [Path] to this audio file. This is only intended for display, [uri] should be favored * instead for accessing the audio file. @@ -284,9 +281,12 @@ interface Song : Music { val durationMs: Long /** The ReplayGain adjustment to apply during playback. */ val replayGainAdjustment: ReplayGainAdjustment + /** The date last modified the audio file was last modified, as a unix epoch timestamp. */ val lastModified: Long /** The date the audio file was added to the device, as a unix epoch timestamp. */ val dateAdded: Long + /** Useful information to quickly obtain the album cover. */ + val cover: Cover.Single /** * The parent [Album]. If the metadata did not specify an album, it's parent directory is used * instead. @@ -319,8 +319,8 @@ interface Album : MusicParent { * [ReleaseType.Album]. */ val releaseType: ReleaseType - /** Cover information from the template song used for the album. */ - val cover: ParentCover + /** Cover information from album's songs. */ + val cover: Cover /** The duration of all songs in the album, in milliseconds. */ val durationMs: Long /** The earliest date a song in this album was added, as a unix epoch timestamp. */ @@ -350,7 +350,7 @@ interface Artist : MusicParent { */ val durationMs: Long? /** Useful information to quickly obtain a (single) cover for a Genre. */ - val cover: ParentCover + val cover: Cover /** The [Genre]s of this artist. */ val genres: List } @@ -366,7 +366,7 @@ interface Genre : MusicParent { /** The total duration of the songs in this genre, in milliseconds. */ val durationMs: Long /** Useful information to quickly obtain a (single) cover for a Genre. */ - val cover: ParentCover + val cover: Cover } /** @@ -380,7 +380,7 @@ interface Playlist : MusicParent { /** The total duration of the songs in this genre, in milliseconds. */ val durationMs: Long /** Useful information to quickly obtain a (single) cover for a Genre. */ - val cover: ParentCover? + val cover: Cover } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 9236b59dc..697c68efc 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -318,6 +318,7 @@ constructor(private val indexer: Indexer, private val musicSettings: MusicSettin } override suspend fun index(worker: IndexingWorker, withCache: Boolean) { + L.d("Begin index [cache=$withCache]") try { indexImpl(withCache) } catch (e: CancellationException) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt index 48488a376..fabff0d99 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/MediaItemTranslation.kt @@ -114,7 +114,7 @@ fun Song.toMediaDescription(context: Context, vararg sugar: Sugar): MediaDescrip .setTitle(name.resolve(context)) .setSubtitle(artists.resolveNames(context)) .setDescription(album.name.resolve(context)) - .setIconUri(cover.mediaStoreCoverUri) + // .setIconUri(cover.mediaStoreCoverUri) .setMediaUri(uri) .setExtras(extras) .build() @@ -134,7 +134,7 @@ fun Album.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem { .setTitle(name.resolve(context)) .setSubtitle(artists.resolveNames(context)) .setDescription(counts) - .setIconUri(cover.single.mediaStoreCoverUri) + // .setIconUri(cover.single.mediaStoreCoverUri) .setExtras(extras) .build() return MediaItem(description, MediaItem.FLAG_BROWSABLE) @@ -162,7 +162,7 @@ fun Artist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem { .setTitle(name.resolve(context)) .setSubtitle(counts) .setDescription(genres.resolveNames(context)) - .setIconUri(cover.single.mediaStoreCoverUri) + // .setIconUri(cover.single.mediaStoreCoverUri) .setExtras(extras) .build() return MediaItem(description, MediaItem.FLAG_BROWSABLE) @@ -182,7 +182,7 @@ fun Genre.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem { .setMediaId(mediaSessionUID.toString()) .setTitle(name.resolve(context)) .setSubtitle(counts) - .setIconUri(cover.single.mediaStoreCoverUri) + // .setIconUri(cover.single.mediaStoreCoverUri) .setExtras(extras) .build() return MediaItem(description, MediaItem.FLAG_BROWSABLE) @@ -203,7 +203,7 @@ fun Playlist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem { .setTitle(name.resolve(context)) .setSubtitle(counts) .setDescription(durationMs.formatDurationDs(true)) - .setIconUri(cover?.single?.mediaStoreCoverUri) + // .setIconUri(cover?.single?.mediaStoreCoverUri) .setExtras(extras) .build() return MediaItem(description, MediaItem.FLAG_BROWSABLE) diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagExtractor.kt index 3f91402ae..7093bf01f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/explore/extractor/TagExtractor.kt @@ -33,7 +33,6 @@ import androidx.media3.exoplayer.LoadingInfo import androidx.media3.exoplayer.analytics.PlayerId import androidx.media3.exoplayer.source.MediaPeriod import androidx.media3.exoplayer.source.MediaSource -import androidx.media3.exoplayer.source.MediaSource.Factory import androidx.media3.exoplayer.source.TrackGroupArray import androidx.media3.exoplayer.upstream.Allocator import androidx.media3.exoplayer.upstream.DefaultAllocator diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/DeviceMusicImpl.kt index 0eb7f0699..9d3ec79a3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/DeviceMusicImpl.kt @@ -19,7 +19,7 @@ package org.oxycblt.auxio.music.stack.interpret.model import kotlin.math.min -import org.oxycblt.auxio.image.extractor.ParentCover +import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.list.sort.Sort import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -49,7 +49,6 @@ class SongImpl(linkedSong: LinkedSong) : Song { 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 @@ -57,6 +56,7 @@ class SongImpl(linkedSong: LinkedSong) : Song { override val replayGainAdjustment = preSong.replayGainAdjustment override val lastModified = preSong.lastModified override val dateAdded = preSong.dateAdded + override val cover = Cover.single(this) override val album = linkedSong.album.resolve(this) override val artists = linkedSong.artists.resolve(this) override val genres = linkedSong.genres.resolve(this) @@ -93,7 +93,7 @@ class AlbumImpl(linkedAlbum: LinkedAlbum) : Album { override val releaseType = preAlbum.releaseType override var durationMs = 0L override var dateAdded = 0L - override lateinit var cover: ParentCover + override var cover = Cover.nil() override var dates: Date.Range? = null override val artists = linkedAlbum.artists.resolve(this) @@ -123,9 +123,7 @@ class AlbumImpl(linkedAlbum: LinkedAlbum) : Album { } } - fun finalize() { - cover = ParentCover(songs.first().cover, songs.map { it.cover }) - } + fun finalize() {} } /** @@ -148,7 +146,7 @@ class ArtistImpl(private val preArtist: PreArtist) : Artist { override var genres = listOf() override var durationMs = 0L - override lateinit var cover: ParentCover + override var cover = Cover.nil() private var hashCode = 31 * uid.hashCode() + preArtist.hashCode() @@ -179,7 +177,7 @@ class ArtistImpl(private val preArtist: PreArtist) : Artist { fun finalize() { explicitAlbums.addAll(albums) implicitAlbums.addAll(songs.mapTo(mutableSetOf()) { it.album } - albums.toSet()) - cover = ParentCover(songs.first().cover, songs.map { it.cover }) + cover = Cover.multi(songs) genres = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING) .genres(songs.flatMapTo(mutableSetOf()) { it.genres }) @@ -199,7 +197,7 @@ class GenreImpl(private val preGenre: PreGenre) : Genre { override val songs = mutableSetOf() override val artists = mutableSetOf() override var durationMs = 0L - override lateinit var cover: ParentCover + override var cover = Cover.nil() private var hashCode = uid.hashCode() @@ -214,10 +212,10 @@ class GenreImpl(private val preGenre: PreGenre) : Genre { songs.add(song) durationMs += song.durationMs hashCode = 31 * hashCode + song.hashCode() - cover = ParentCover(song.cover, emptyList()) } fun finalize() { + cover = Cover.multi(songs) artists.addAll(songs.flatMapTo(mutableSetOf()) { it.artists }) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/PlaylistImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/PlaylistImpl.kt index 9816a6a77..cd5de9c06 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/PlaylistImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/model/PlaylistImpl.kt @@ -18,7 +18,7 @@ package org.oxycblt.auxio.music.stack.interpret.model -import org.oxycblt.auxio.image.extractor.ParentCover +import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.stack.interpret.linker.LinkedPlaylist @@ -29,7 +29,7 @@ class PlaylistImpl(linkedPlaylist: LinkedPlaylist) : Playlist { override val name: Name.Known = prePlaylist.name override val songs = linkedPlaylist.songs.resolve(this) override val durationMs = songs.sumOf { it.durationMs } - override val cover = songs.takeIf { it.isNotEmpty() }?.let { ParentCover.from(it.first(), it) } + override val cover = Cover.multi(songs) private var hashCode = uid.hashCode() init { diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PreMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PreMusic.kt index fe89f7975..15c141ff5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PreMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/PreMusic.kt @@ -20,7 +20,6 @@ package org.oxycblt.auxio.music.stack.interpret.prepare import android.net.Uri import java.util.UUID -import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.info.Date @@ -41,7 +40,6 @@ data class PreSong( val disc: Disc?, val date: Date?, val uri: Uri, - val cover: Cover, val path: Path, val mimeType: MimeType, val size: Long, diff --git a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/Preparer.kt b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/Preparer.kt index 53cac934c..d67948da4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/Preparer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/stack/interpret/prepare/Preparer.kt @@ -22,7 +22,6 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.oxycblt.auxio.R -import org.oxycblt.auxio.image.extractor.Cover import org.oxycblt.auxio.music.info.Disc import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.ReleaseType @@ -70,7 +69,6 @@ class PreparerImpl @Inject constructor() : Preparer { disc = audioFile.disc?.let { Disc(it, audioFile.subtitle) }, date = audioFile.date, uri = uri, - cover = inferCover(audioFile), path = need(audioFile, "path", audioFile.deviceFile.path), mimeType = MimeType(need(audioFile, "mime type", audioFile.deviceFile.mimeType), null), @@ -92,10 +90,6 @@ class PreparerImpl @Inject constructor() : Preparer { private fun need(audioFile: AudioFile, what: String, value: T?) = requireNotNull(value) { "Invalid $what for song ${audioFile.deviceFile.path}: No $what" } - private fun inferCover(audioFile: AudioFile): Cover { - return Cover.Embedded(audioFile.deviceFile.uri, audioFile.deviceFile.uri, "") - } - private fun makePreAlbum( audioFile: AudioFile, individualPreArtists: List,