image: implement extractors and new cover data

This commit is contained in:
Alexander Capehart 2024-11-27 20:11:55 -07:00
parent 37697abfce
commit 7a7774a4db
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
24 changed files with 582 additions and 450 deletions

View file

@ -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<Song>, desc: String, @DrawableRes errorRes: Int) =
bindImpl(Cover.order(songs), desc, errorRes)
bindImpl(Cover.multi(songs), desc, errorRes)
private fun bindImpl(covers: List<Cover>, 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())

View file

@ -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<Collection<Cover>> {
override fun key(data: Collection<Cover>, options: Options) =
"${data.map { it.key }.hashCode()}"
class CoverKeyer @Inject constructor() : Keyer<Cover> {
override fun key(data: Cover, options: Options) = "${data.key}&${options.size}"
}
class CoverFetcher
private constructor(
private val covers: Collection<Cover>,
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<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(private val coverExtractor: CoverExtractor) :
Fetcher.Factory<Collection<Cover>> {
override fun create(data: Collection<Cover>, options: Options, imageLoader: ImageLoader) =
CoverFetcher(data, options.size, coverExtractor)
Fetcher.Factory<Cover> {
override fun create(data: Cover, options: Options, imageLoader: ImageLoader) =
CoverFetcher(options.context, data, options.size, coverExtractor)
}
}

View file

@ -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<Single>) : 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<Song>) =
fun nil() = Multi(listOf())
fun single(song: Song) = Single(song)
fun multi(songs: Collection<Song>) = order(songs).run { Multi(this) }
private fun order(songs: Collection<Song>) =
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<Cover>) {
companion object {
fun from(song: Song, songs: Collection<Song>) = from(song.cover, songs)
fun from(src: Cover, songs: Collection<Song>) = ParentCover(src, Cover.order(songs))
.map { it.value.first().cover }
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Cover>, size: Size): FetchResult? {
val streams = mutableListOf<InputStream>()
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<InputStream>, 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
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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) }
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}

View file

@ -1,41 +1,63 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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) {
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 ->
val compressedStream =
ByteArrayOutputStream().use { outputStream ->
bitmap.compress(COVER_CACHE_FORMAT, 80, outputStream)
ByteArrayInputStream(outputStream.toByteArray())
}
@ -47,9 +69,7 @@ class CoverCacheImpl @Inject constructor(
StoredCover(
uid = song.uid,
lastModified = song.lastModified,
perceptualHash = perceptualHash
)
)
perceptualHash = perceptualHash))
}
writeSuccess
@ -59,7 +79,8 @@ class CoverCacheImpl @Inject constructor(
private companion object {
@Suppress("DEPRECATION")
val COVER_CACHE_FORMAT = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val COVER_CACHE_FORMAT =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Bitmap.CompressFormat.WEBP_LOSSY
} else {
Bitmap.CompressFormat.WEBP

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.image.stack.cache
import android.graphics.Bitmap
@ -45,5 +63,4 @@ class PerceptualHashImpl : PerceptualHash {
return hash.toString(16)
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
)

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<CoverSource>)
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
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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))
}

View file

@ -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<Genre>
}
@ -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
}
/**

View file

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

View file

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

View file

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

View file

@ -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<Genre>()
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<Song>()
override val artists = mutableSetOf<Artist>()
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 })
}
}

View file

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

View file

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

View file

@ -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 <T> 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<PreArtist>,