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 c34b69d5c..eddd90b9d 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
@@ -34,6 +34,8 @@ interface StackModule {
@Singleton @Binds fun appFiles(impl: AppFilesImpl): AppFiles
@Binds fun coverCache(cache: CoverCacheImpl): CoverCache
+
+ @Binds fun coverIdentifier(identifierImpl: CoverIdentifierImpl): CoverIdentifier
}
@Module
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 88bd4e008..3199b132e 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
@@ -25,73 +25,60 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import javax.inject.Inject
-import kotlin.math.min
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
import org.oxycblt.auxio.image.Cover
interface CoverCache {
suspend fun read(cover: Cover.Single): InputStream?
- suspend fun write(cover: Cover.Single, inputStream: InputStream): InputStream?
+ suspend fun write(cover: Cover.Single, data: ByteArray): InputStream?
}
class CoverCacheImpl
@Inject
-constructor(private val storedCoversDao: StoredCoversDao, private val appFiles: AppFiles) :
- CoverCache {
+constructor(
+ private val coverIdentifier: CoverIdentifier,
+ private val storedCoversDao: StoredCoversDao,
+ private val appFiles: AppFiles
+) : CoverCache {
override suspend fun read(cover: Cover.Single): InputStream? {
val perceptualHash =
- storedCoversDao.getCoverFile(cover.uid, cover.lastModified) ?: return null
+ storedCoversDao.getStoredCover(cover.uid, cover.lastModified) ?: return null
return appFiles.read(fileName(perceptualHash))
}
- override suspend fun write(cover: Cover.Single, inputStream: InputStream): InputStream? {
- val id =
- withContext(Dispatchers.IO) {
- val available = inputStream.available()
- val skip = min(available / 2L, available - COVER_KEY_SAMPLE.toLong())
- inputStream.skip(skip)
- val bytes = ByteArray(COVER_KEY_SAMPLE)
- inputStream.read(bytes)
- inputStream.reset()
- @OptIn(ExperimentalStdlibApi::class) bytes.toHexString()
- }
+ override suspend fun write(cover: Cover.Single, data: ByteArray): InputStream? {
+ val id = coverIdentifier.identify(data)
val file = fileName(id)
if (!appFiles.exists(file)) {
- val transcoded = transcodeImage(inputStream, FORMAT_WEBP)
+ val transcoded = transcodeImage(data, FORMAT_WEBP)
val writeSuccess = appFiles.write(fileName(id), transcoded)
if (!writeSuccess) {
return null
}
}
- storedCoversDao.setCoverFile(
- StoredCover(uid = cover.uid, lastModified = cover.lastModified, perceptualHash = id))
+ storedCoversDao.setStoredCover(
+ StoredCover(uid = cover.uid, lastModified = cover.lastModified, coverId = id))
return appFiles.read(file)
}
private fun fileName(id: String) = "cover_$id"
- private fun transcodeImage(
- inputStream: InputStream,
- targetFormat: Bitmap.CompressFormat
- ): InputStream {
+ private fun transcodeImage(data: ByteArray, targetFormat: Bitmap.CompressFormat): InputStream {
val options =
BitmapFactory.Options().apply {
inJustDecodeBounds = true
- BitmapFactory.decodeStream(inputStream, null, this)
+ BitmapFactory.decodeByteArray(data, 0, data.size, this)
}
options.inSampleSize = calculateInSampleSize(options, 750, 750)
- inputStream.reset()
val bitmap =
- BitmapFactory.decodeStream(
- inputStream, null, options.apply { inJustDecodeBounds = false })
+ BitmapFactory.decodeByteArray(
+ data, 0, data.size, options.apply { inJustDecodeBounds = false })
return ByteArrayOutputStream().use { outputStream ->
bitmap?.compress(targetFormat, 80, outputStream)
@@ -119,7 +106,6 @@ constructor(private val storedCoversDao: StoredCoversDao, private val appFiles:
}
private companion object {
- const val COVER_KEY_SAMPLE = 32
val FORMAT_WEBP =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Bitmap.CompressFormat.WEBP_LOSSY
diff --git a/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CoverIdentifier.kt b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CoverIdentifier.kt
new file mode 100644
index 000000000..69ae53d60
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/image/stack/cache/CoverIdentifier.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2024 Auxio Project
+ * CoverIdentifier.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 java.security.MessageDigest
+import javax.inject.Inject
+
+interface CoverIdentifier {
+ suspend fun identify(data: ByteArray): String
+}
+
+class CoverIdentifierImpl @Inject constructor() : CoverIdentifier {
+ @OptIn(ExperimentalStdlibApi::class)
+ override suspend fun identify(data: ByteArray): String {
+ val digest =
+ MessageDigest.getInstance("MD5").run {
+ update(data)
+ digest()
+ }
+ return digest.toHexString()
+ }
+}
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 f168cdf95..b9fbae075 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
@@ -36,18 +36,14 @@ abstract class StoredCoversDatabase : RoomDatabase() {
@Dao
interface StoredCoversDao {
- @Query(
- "SELECT perceptualHash FROM StoredCover WHERE uid = :uid AND lastModified = :lastModified")
+ @Query("SELECT coverId FROM StoredCover WHERE uid = :uid AND lastModified = :lastModified")
@TypeConverters(Music.UID.TypeConverters::class)
- fun getCoverFile(uid: Music.UID, lastModified: Long): String?
+ suspend fun getStoredCover(uid: Music.UID, lastModified: Long): String?
- @Insert(onConflict = OnConflictStrategy.REPLACE) fun setCoverFile(storedCover: StoredCover)
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun setStoredCover(storedCover: StoredCover)
}
@Entity
@TypeConverters(Music.UID.TypeConverters::class)
-data class StoredCover(
- @PrimaryKey val uid: Music.UID,
- val lastModified: Long,
- val perceptualHash: String
-)
+data class StoredCover(@PrimaryKey val uid: Music.UID, val lastModified: Long, val coverId: String)
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
index 688476128..c2920008d 100644
--- 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
@@ -22,20 +22,17 @@ import android.content.Context
import android.media.MediaMetadataRetriever
import android.net.Uri
import dagger.hilt.android.qualifiers.ApplicationContext
-import java.io.InputStream
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AOSPCoverSource @Inject constructor(@ApplicationContext private val context: Context) :
CoverSource {
- override suspend fun extract(fileUri: Uri): InputStream? {
+ override suspend fun extract(fileUri: Uri): ByteArray? {
val mediaMetadataRetriever = MediaMetadataRetriever()
- val cover =
- withContext(Dispatchers.IO) {
- mediaMetadataRetriever.setDataSource(context, fileUri)
- mediaMetadataRetriever.embeddedPicture
- } ?: return null
- return cover.inputStream()
+ return withContext(Dispatchers.IO) {
+ mediaMetadataRetriever.setDataSource(context, fileUri)
+ mediaMetadataRetriever.embeddedPicture
+ }
}
}
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
index 885f1618e..d491e1780 100644
--- 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
@@ -19,23 +19,22 @@
package org.oxycblt.auxio.image.stack.extractor
import android.net.Uri
-import java.io.InputStream
import javax.inject.Inject
import org.oxycblt.auxio.image.Cover
interface CoverExtractor {
- suspend fun extract(cover: Cover.Single): InputStream?
+ suspend fun extract(cover: Cover.Single): ByteArray?
}
data class CoverSources(val sources: List)
interface CoverSource {
- suspend fun extract(fileUri: Uri): InputStream?
+ suspend fun extract(fileUri: Uri): ByteArray?
}
class CoverExtractorImpl @Inject constructor(private val coverSources: CoverSources) :
CoverExtractor {
- override suspend fun extract(cover: Cover.Single): InputStream? {
+ override suspend fun extract(cover: Cover.Single): ByteArray? {
for (coverSource in coverSources.sources) {
val stream = coverSource.extract(cover.uri)
if (stream != 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
index 576d26a4a..79875f836 100644
--- 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
@@ -26,15 +26,13 @@ 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? {
+ override suspend fun extract(fileUri: Uri): ByteArray? {
val tracks =
MetadataRetriever.retrieveMetadata(mediaSourceFactory, MediaItem.fromUri(fileUri))
.asDeferred()
@@ -52,8 +50,8 @@ constructor(private val mediaSourceFactory: MediaSource.Factory) : CoverSource {
return findCoverDataInMetadata(metadata)
}
- private fun findCoverDataInMetadata(metadata: Metadata): InputStream? {
- var stream: ByteArrayInputStream? = null
+ private fun findCoverDataInMetadata(metadata: Metadata): ByteArray? {
+ var fallbackPic: ByteArray? = null
for (i in 0 until metadata.length()) {
// We can only extract pictures from two tags with this method, ID3v2's APIC or
@@ -74,13 +72,12 @@ constructor(private val mediaSourceFactory: MediaSource.Factory) : CoverSource {
}
if (type == MediaMetadata.PICTURE_TYPE_FRONT_COVER) {
- stream = ByteArrayInputStream(pic)
- break
- } else if (stream == null) {
- stream = ByteArrayInputStream(pic)
+ return pic
+ } else if (fallbackPic == null) {
+ fallbackPic = pic
}
}
- return stream
+ return fallbackPic
}
}