diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt
deleted file mode 100644
index 714dd1994..000000000
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverModule.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (c) 2024 Auxio Project
- * CoverModule.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.covers
-
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import org.oxycblt.musikr.covers.embedded.CoverIdentifier
-
-@Module
-@InstallIn(SingletonComponent::class)
-interface CoverModule {
- @Binds fun configCovers(impl: SettingCoversImpl): SettingCovers
-}
-
-@Module
-@InstallIn(SingletonComponent::class)
-class CoverProvidesModule {
- @Provides fun identifier(): CoverIdentifier = CoverIdentifier.md5()
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt
index a5383621b..735dd1cda 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/covers/CoverSilo.kt
@@ -19,28 +19,6 @@
package org.oxycblt.auxio.image.covers
import java.util.UUID
-import org.oxycblt.musikr.covers.embedded.CoverParams
-
-data class CoverSilo(val revision: UUID, val params: CoverParams?) {
- override fun toString() =
- "${revision}${params?.let { ".${params.resolution}.${params.quality}" } ?: "" }"
-
- companion object {
- fun parse(silo: String): CoverSilo? {
- val parts = silo.split('.')
- if (parts.size != 1 && parts.size != 3) {
- return null
- }
- val revision = parts[0].toUuidOrNull() ?: return null
- if (parts.size > 1) {
- val resolution = parts[1].toIntOrNull() ?: return null
- val quality = parts[2].toIntOrNull() ?: return null
- return CoverSilo(revision, CoverParams.of(resolution, quality))
- }
- return CoverSilo(revision, null)
- }
- }
-}
private fun String.toUuidOrNull(): UUID? =
try {
diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt
index 079ed5f6c..df39b5981 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/covers/NullCovers.kt
@@ -18,20 +18,20 @@
package org.oxycblt.auxio.image.covers
-import android.content.Context
import org.oxycblt.musikr.covers.Cover
import org.oxycblt.musikr.covers.CoverResult
import org.oxycblt.musikr.covers.MutableCovers
+import org.oxycblt.musikr.covers.stored.CoverStorage
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
-class NullCovers(private val context: Context) : MutableCovers {
+class NullCovers(private val storage: CoverStorage) : MutableCovers {
override suspend fun obtain(id: String) = CoverResult.Hit(NullCover)
override suspend fun create(file: DeviceFile, metadata: Metadata) = CoverResult.Hit(NullCover)
override suspend fun cleanup(excluding: Collection) {
- context.coversDir().listFiles()?.forEach { it.deleteRecursively() }
+ storage.ls(setOf()).map { storage.rm(it) }
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverUtil.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/RevisionedTranscoding.kt
similarity index 69%
rename from app/src/main/java/org/oxycblt/auxio/image/covers/CoverUtil.kt
rename to app/src/main/java/org/oxycblt/auxio/image/covers/RevisionedTranscoding.kt
index 67058e8b6..95980c42d 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/CoverUtil.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/covers/RevisionedTranscoding.kt
@@ -1,6 +1,6 @@
/*
- * Copyright (c) 2024 Auxio Project
- * CoverUtil.kt is part of Auxio.
+ * Copyright (c) 2025 Auxio Project
+ * RevisionedTranscoding.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
@@ -18,9 +18,9 @@
package org.oxycblt.auxio.image.covers
-import android.content.Context
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
+import java.util.UUID
+import org.oxycblt.musikr.covers.stored.Transcoding
-suspend fun Context.coversDir() =
- withContext(Dispatchers.IO) { filesDir.resolve("covers").apply { mkdirs() } }
+class RevisionedTranscoding(revision: UUID, private val inner: Transcoding) : Transcoding by inner {
+ override val tag = "_$revision${inner.tag}"
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt
index 774eeb65c..85cc24f1a 100644
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt
+++ b/app/src/main/java/org/oxycblt/auxio/image/covers/SettingCovers.kt
@@ -19,6 +19,7 @@
package org.oxycblt.auxio.image.covers
import android.content.Context
+import android.graphics.Bitmap
import java.util.UUID
import javax.inject.Inject
import org.oxycblt.auxio.image.CoverMode
@@ -28,16 +29,21 @@ import org.oxycblt.musikr.covers.Covers
import org.oxycblt.musikr.covers.FDCover
import org.oxycblt.musikr.covers.MutableCovers
import org.oxycblt.musikr.covers.embedded.CoverIdentifier
-import org.oxycblt.musikr.covers.embedded.CoverParams
+import org.oxycblt.musikr.covers.embedded.EmbeddedCovers
import org.oxycblt.musikr.covers.fs.FSCovers
import org.oxycblt.musikr.covers.fs.MutableFSCovers
+import org.oxycblt.musikr.covers.stored.Compress
+import org.oxycblt.musikr.covers.stored.CoverStorage
+import org.oxycblt.musikr.covers.stored.MutableStoredCovers
+import org.oxycblt.musikr.covers.stored.NoTranscoding
+import org.oxycblt.musikr.covers.stored.StoredCovers
interface SettingCovers {
suspend fun mutate(context: Context, revision: UUID): MutableCovers
companion object {
- fun immutable(context: Context): Covers =
- Covers.chain(BaseSiloedCovers(context), FSCovers(context))
+ suspend fun immutable(context: Context): Covers =
+ Covers.chain(StoredCovers(CoverStorage.at(context.coversDir())), FSCovers(context))
}
}
@@ -45,17 +51,22 @@ class SettingCoversImpl
@Inject
constructor(private val imageSettings: ImageSettings, private val identifier: CoverIdentifier) :
SettingCovers {
- override suspend fun mutate(context: Context, revision: UUID): MutableCovers =
- when (imageSettings.coverMode) {
- CoverMode.OFF -> NullCovers(context)
- CoverMode.SAVE_SPACE -> siloedCovers(context, revision, CoverParams.of(500, 70))
- CoverMode.BALANCED -> siloedCovers(context, revision, CoverParams.of(750, 85))
- CoverMode.HIGH_QUALITY -> siloedCovers(context, revision, CoverParams.of(1000, 100))
- CoverMode.AS_IS -> siloedCovers(context, revision, null)
- }
-
- private suspend fun siloedCovers(context: Context, revision: UUID, with: CoverParams?) =
- MutableCovers.chain(
- MutableSiloedCovers.from(context, CoverSilo(revision, with), identifier),
- MutableFSCovers(context))
+ override suspend fun mutate(context: Context, revision: UUID): MutableCovers {
+ val coverStorage = CoverStorage.at(context.coversDir())
+ val transcoding =
+ when (imageSettings.coverMode) {
+ CoverMode.OFF -> return NullCovers(coverStorage)
+ CoverMode.SAVE_SPACE -> Compress(Bitmap.CompressFormat.JPEG, 500, 70)
+ CoverMode.BALANCED -> Compress(Bitmap.CompressFormat.JPEG, 750, 85)
+ CoverMode.HIGH_QUALITY -> Compress(Bitmap.CompressFormat.JPEG, 1000, 100)
+ CoverMode.AS_IS -> NoTranscoding
+ }
+ val revisionedTranscoding = RevisionedTranscoding(revision, transcoding)
+ val storedCovers =
+ MutableStoredCovers(EmbeddedCovers(identifier), coverStorage, revisionedTranscoding)
+ val fsCovers = MutableFSCovers(context)
+ return MutableCovers.chain(storedCovers, fsCovers)
+ }
}
+
+private fun Context.coversDir() = filesDir.resolve("covers")
diff --git a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt b/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt
deleted file mode 100644
index e1bc4d622..000000000
--- a/app/src/main/java/org/oxycblt/auxio/image/covers/SiloedCovers.kt
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (c) 2024 Auxio Project
- * SiloedCovers.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.covers
-
-import android.content.Context
-import java.io.File
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import org.oxycblt.musikr.covers.Cover
-import org.oxycblt.musikr.covers.CoverResult
-import org.oxycblt.musikr.covers.Covers
-import org.oxycblt.musikr.covers.FDCover
-import org.oxycblt.musikr.covers.MutableCovers
-import org.oxycblt.musikr.covers.embedded.CoverFormat
-import org.oxycblt.musikr.covers.embedded.CoverIdentifier
-import org.oxycblt.musikr.covers.embedded.EmbeddedCovers
-import org.oxycblt.musikr.covers.embedded.MutableEmbeddedCovers
-import org.oxycblt.musikr.fs.app.AppFS
-import org.oxycblt.musikr.fs.device.DeviceFile
-import org.oxycblt.musikr.metadata.Metadata
-
-class BaseSiloedCovers(private val context: Context) : Covers {
- override suspend fun obtain(id: String): CoverResult {
- val siloedId = SiloedCoverId.parse(id) ?: return CoverResult.Miss()
- val core = SiloCore.from(context, siloedId.silo)
- val embeddedCovers = EmbeddedCovers(core.files, core.format)
- return when (val result = embeddedCovers.obtain(siloedId.id)) {
- is CoverResult.Hit -> CoverResult.Hit(SiloedCover(siloedId.silo, result.cover))
- is CoverResult.Miss -> CoverResult.Miss()
- }
- }
-}
-
-open class SiloedCovers(private val silo: CoverSilo, private val embeddedCovers: EmbeddedCovers) :
- Covers {
- override suspend fun obtain(id: String): CoverResult {
- val coverId = SiloedCoverId.parse(id) ?: return CoverResult.Miss()
- if (silo != coverId.silo) return CoverResult.Miss()
- return when (val result = embeddedCovers.obtain(coverId.id)) {
- is CoverResult.Hit -> CoverResult.Hit(SiloedCover(silo, result.cover))
- is CoverResult.Miss -> CoverResult.Miss()
- }
- }
-
- companion object {
- suspend fun from(context: Context, silo: CoverSilo): SiloedCovers {
- val core = SiloCore.from(context, silo)
- return SiloedCovers(silo, EmbeddedCovers(core.files, core.format))
- }
- }
-}
-
-class MutableSiloedCovers
-private constructor(
- private val rootDir: File,
- private val silo: CoverSilo,
- private val fileCovers: MutableEmbeddedCovers
-) : SiloedCovers(silo, fileCovers), MutableCovers {
- override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult =
- when (val result = fileCovers.create(file, metadata)) {
- is CoverResult.Hit -> CoverResult.Hit(SiloedCover(silo, result.cover))
- is CoverResult.Miss -> CoverResult.Miss()
- }
-
- override suspend fun cleanup(excluding: Collection) {
- fileCovers.cleanup(excluding.filterIsInstance().map { it.innerCover })
-
- // Destroy old revisions no longer being used.
- withContext(Dispatchers.IO) {
- val exclude = silo.toString()
- rootDir.listFiles { file -> file.name != exclude }?.forEach { it.deleteRecursively() }
- }
- }
-
- companion object {
- suspend fun from(
- context: Context,
- silo: CoverSilo,
- coverIdentifier: CoverIdentifier
- ): MutableSiloedCovers {
- val core = SiloCore.from(context, silo)
- return MutableSiloedCovers(
- core.rootDir, silo, MutableEmbeddedCovers(core.files, core.format, coverIdentifier))
- }
- }
-}
-
-data class SiloedCover(private val silo: CoverSilo, val innerCover: FDCover) :
- FDCover by innerCover {
- private val innerId = SiloedCoverId(silo, innerCover.id)
- override val id = innerId.toString()
-}
-
-data class SiloedCoverId(val silo: CoverSilo, val id: String) {
- override fun toString() = "$id@$silo"
-
- companion object {
- fun parse(id: String): SiloedCoverId? {
- val parts = id.split('@')
- if (parts.size != 2) return null
- val silo = CoverSilo.parse(parts[1]) ?: return null
- return SiloedCoverId(silo, parts[0])
- }
- }
-}
-
-private data class SiloCore(val rootDir: File, val files: AppFS, val format: CoverFormat) {
- companion object {
- suspend fun from(context: Context, silo: CoverSilo): SiloCore {
- val rootDir: File
- val revisionDir: File
- withContext(Dispatchers.IO) {
- rootDir = context.coversDir()
- revisionDir = rootDir.resolve(silo.toString()).apply { mkdirs() }
- }
- val files = AppFS.at(revisionDir)
- val format = silo.params?.let(CoverFormat::jpeg) ?: CoverFormat.asIs()
- return SiloCore(rootDir, files, format)
- }
- }
-}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt
index adef4cf44..3a81665ec 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/Covers.kt
@@ -99,6 +99,10 @@ interface FDCover : Cover {
suspend fun fd(): ParcelFileDescriptor?
}
+interface MemoryCover : Cover {
+ fun data(): ByteArray
+}
+
class CoverCollection private constructor(val covers: List) {
override fun hashCode() = covers.hashCode()
diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt
deleted file mode 100644
index e4ae6e8cf..000000000
--- a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverParams.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (c) 2024 Auxio Project
- * CoverParams.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.musikr.covers.embedded
-
-class CoverParams private constructor(val resolution: Int, val quality: Int) {
- override fun hashCode() = 31 * resolution + quality
-
- override fun equals(other: Any?) =
- other is CoverParams && other.resolution == resolution && other.quality == quality
-
- companion object {
- fun of(resolution: Int, quality: Int): CoverParams {
- check(resolution > 0) { "Resolution must be positive" }
- check(quality in 0..100) { "Quality must be between 0 and 100" }
- return CoverParams(resolution, quality)
- }
- }
-}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt
index e1aa5f24f..4607ef444 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/EmbeddedCovers.kt
@@ -18,51 +18,36 @@
package org.oxycblt.musikr.covers.embedded
+import java.io.ByteArrayInputStream
import org.oxycblt.musikr.covers.Cover
import org.oxycblt.musikr.covers.CoverResult
-import org.oxycblt.musikr.covers.Covers
-import org.oxycblt.musikr.covers.FDCover
+import org.oxycblt.musikr.covers.MemoryCover
import org.oxycblt.musikr.covers.MutableCovers
-import org.oxycblt.musikr.fs.app.AppFS
-import org.oxycblt.musikr.fs.app.AppFile
import org.oxycblt.musikr.fs.device.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
-open class EmbeddedCovers(private val appFS: AppFS, private val coverFormat: CoverFormat) :
- Covers {
- override suspend fun obtain(id: String): CoverResult {
- val file = appFS.find(getFileName(id))
- return if (file != null) {
- CoverResult.Hit(InternalCoverImpl(id, file))
- } else {
- CoverResult.Miss()
- }
- }
+class EmbeddedCovers(private val coverIdentifier: CoverIdentifier) : MutableCovers {
+ override suspend fun obtain(id: String): CoverResult = CoverResult.Miss()
- protected fun getFileName(id: String) = "$id.${coverFormat.extension}"
-}
-
-class MutableEmbeddedCovers(
- private val appFS: AppFS,
- private val coverFormat: CoverFormat,
- private val coverIdentifier: CoverIdentifier
-) : EmbeddedCovers(appFS, coverFormat), MutableCovers {
- override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult {
+ override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult {
val data = metadata.cover ?: return CoverResult.Miss()
val id = coverIdentifier.identify(data)
- val coverFile = appFS.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
- return CoverResult.Hit(InternalCoverImpl(id, coverFile))
+ return CoverResult.Hit(EmbeddedCover(id, data))
}
- override suspend fun cleanup(excluding: Collection) {
- val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) }
- appFS.deleteWhere { it !in used }
+ override suspend fun cleanup(excluding: Collection) {}
+}
+
+private class EmbeddedCover(override val id: String, private val data: ByteArray) : MemoryCover {
+ override suspend fun open() = ByteArrayInputStream(data)
+
+ override fun data() = data
+
+ override fun hashCode(): Int = id.hashCode()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is EmbeddedCover) return false
+ return id == other.id
}
}
-
-private data class InternalCoverImpl(override val id: String, private val appFile: AppFile) :
- FDCover {
- override suspend fun fd() = appFile.fd()
-
- override suspend fun open() = appFile.open()
-}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt
similarity index 68%
rename from musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt
rename to musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt
index 50b423c08..11382225d 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/fs/app/AppFS.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/CoverStorage.kt
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2024 Auxio Project
- * AppFS.kt is part of Auxio.
+ * CoverStorage.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
@@ -16,40 +16,36 @@
* along with this program. If not, see .
*/
-package org.oxycblt.musikr.fs.app
+package org.oxycblt.musikr.covers.stored
import android.os.ParcelFileDescriptor
import java.io.File
import java.io.IOException
-import java.io.InputStream
import java.io.OutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
+import org.oxycblt.musikr.covers.FDCover
-interface AppFS {
- suspend fun find(name: String): AppFile?
+interface CoverStorage {
+ suspend fun find(name: String): FDCover?
- suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile
+ suspend fun write(name: String, block: suspend (OutputStream) -> Unit): FDCover
- suspend fun deleteWhere(block: (String) -> Boolean)
+ suspend fun ls(exclude: Set): List
+
+ suspend fun rm(file: String)
companion object {
- suspend fun at(dir: File): AppFS {
+ suspend fun at(dir: File): CoverStorage {
withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) }
- return AppFSImpl(dir)
+ return CoverStorageImpl(dir)
}
}
}
-interface AppFile {
- suspend fun fd(): ParcelFileDescriptor?
-
- suspend fun open(): InputStream?
-}
-
-private class AppFSImpl(private val dir: File) : AppFS {
+private class CoverStorageImpl(private val dir: File) : CoverStorage {
private val fileMutexes = mutableMapOf()
private val mapMutex = Mutex()
@@ -57,16 +53,16 @@ private class AppFSImpl(private val dir: File) : AppFS {
return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } }
}
- override suspend fun find(name: String): AppFile? =
+ override suspend fun find(name: String): FDCover? =
withContext(Dispatchers.IO) {
try {
- File(dir, name).takeIf { it.exists() }?.let { AppFileImpl(it) }
+ File(dir, name).takeIf { it.exists() }?.let { StoredCover(it) }
} catch (e: IOException) {
null
}
}
- override suspend fun write(name: String, block: suspend (OutputStream) -> Unit): AppFile {
+ override suspend fun write(name: String, block: suspend (OutputStream) -> Unit): FDCover {
val fileMutex = getMutexForFile(name)
return fileMutex.withLock {
val targetFile = File(dir, name)
@@ -77,26 +73,31 @@ private class AppFSImpl(private val dir: File) : AppFS {
try {
tempFile.outputStream().use { block(it) }
tempFile.renameTo(targetFile)
- AppFileImpl(targetFile)
+ StoredCover(targetFile)
} catch (e: IOException) {
tempFile.delete()
throw e
}
}
} else {
- AppFileImpl(targetFile)
+ StoredCover(targetFile)
}
}
}
- override suspend fun deleteWhere(block: (String) -> Boolean) {
+ override suspend fun ls(exclude: Set): List =
withContext(Dispatchers.IO) {
- dir.listFiles { file -> block(file.name) }?.forEach { it.deleteRecursively() }
+ dir.listFiles()?.map { it.name }?.filter { exclude.contains(it) } ?: emptyList()
}
+
+ override suspend fun rm(file: String) {
+ withContext(Dispatchers.IO) { File(dir, file).delete() }
}
}
-private data class AppFileImpl(private val file: File) : AppFile {
+private data class StoredCover(private val file: File) : FDCover {
+ override val id: String = file.name
+
override suspend fun fd() =
withContext(Dispatchers.IO) {
try {
@@ -107,4 +108,8 @@ private data class AppFileImpl(private val file: File) : AppFile {
}
override suspend fun open() = withContext(Dispatchers.IO) { file.inputStream() }
+
+ override fun equals(other: Any?) = other is StoredCover && file == other.file
+
+ override fun hashCode() = file.hashCode()
}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt
new file mode 100644
index 000000000..4bf88b0d4
--- /dev/null
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/StoredCovers.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2025 Auxio Project
+ * StoredCovers.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.musikr.covers.stored
+
+import org.oxycblt.musikr.covers.Cover
+import org.oxycblt.musikr.covers.CoverResult
+import org.oxycblt.musikr.covers.Covers
+import org.oxycblt.musikr.covers.FDCover
+import org.oxycblt.musikr.covers.MemoryCover
+import org.oxycblt.musikr.covers.MutableCovers
+import org.oxycblt.musikr.fs.device.DeviceFile
+import org.oxycblt.musikr.metadata.Metadata
+
+class StoredCovers(private val coverStorage: CoverStorage) : Covers {
+ override suspend fun obtain(id: String): CoverResult {
+ val cover = coverStorage.find(id) ?: return CoverResult.Miss()
+ return CoverResult.Hit(cover)
+ }
+}
+
+class MutableStoredCovers(
+ private val src: MutableCovers,
+ private val coverStorage: CoverStorage,
+ private val transcoding: Transcoding
+) : MutableCovers {
+ private val base = StoredCovers(coverStorage)
+
+ override suspend fun obtain(id: String): CoverResult = base.obtain(id)
+
+ override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult {
+ val cover =
+ when (val cover = src.create(file, metadata)) {
+ is CoverResult.Hit -> cover.cover
+ is CoverResult.Miss -> return CoverResult.Miss()
+ }
+ val coverFile = coverStorage.write(cover.id + transcoding.tag) { it.write(cover.data()) }
+ return CoverResult.Hit(coverFile)
+ }
+
+ override suspend fun cleanup(excluding: Collection) {
+ src.cleanup(excluding)
+ val used = excluding.mapTo(mutableSetOf()) { it.id }
+ val unused = coverStorage.ls(exclude = used).filter { it !in used }
+ for (file in unused) {
+ coverStorage.rm(file)
+ }
+ }
+}
diff --git a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/Transcoding.kt
similarity index 61%
rename from musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt
rename to musikr/src/main/java/org/oxycblt/musikr/covers/stored/Transcoding.kt
index 6ed9bce02..d2e347cc7 100644
--- a/musikr/src/main/java/org/oxycblt/musikr/covers/embedded/CoverFormat.kt
+++ b/musikr/src/main/java/org/oxycblt/musikr/covers/stored/Transcoding.kt
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2025 Auxio Project
- * CoverFormat.kt is part of Auxio.
+ * Transcoding.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
@@ -16,40 +16,47 @@
* along with this program. If not, see .
*/
-package org.oxycblt.musikr.covers.embedded
+package org.oxycblt.musikr.covers.stored
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.io.OutputStream
-abstract class CoverFormat {
- internal abstract val extension: String
+interface Transcoding {
+ val tag: String
- internal abstract fun transcodeInto(data: ByteArray, output: OutputStream): Boolean
+ fun transcodeInto(data: ByteArray, output: OutputStream)
+}
- companion object {
- fun jpeg(params: CoverParams): CoverFormat =
- CompressingCoverFormat("jpg", params, Bitmap.CompressFormat.JPEG)
+object NoTranscoding : Transcoding {
+ override val tag = ".img"
- fun asIs(): CoverFormat = AsIsCoverFormat()
+ override fun transcodeInto(data: ByteArray, output: OutputStream) {
+ output.write(data)
}
}
-private class CompressingCoverFormat(
- override val extension: String,
- private val params: CoverParams,
+class Compress(
private val format: Bitmap.CompressFormat,
-) : CoverFormat() {
- override fun transcodeInto(data: ByteArray, output: OutputStream) =
+ private val resolution: Int,
+ private val quality: Int,
+) : Transcoding {
+ override val tag = "_${resolution}x${quality}.${format.name.lowercase()}"
+
+ override fun transcodeInto(data: ByteArray, output: OutputStream) {
BitmapFactory.Options().run {
inJustDecodeBounds = true
BitmapFactory.decodeByteArray(data, 0, data.size, this)
- inSampleSize = calculateInSampleSize(params.resolution)
+ inSampleSize = calculateInSampleSize(resolution)
inJustDecodeBounds = false
- val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, this) ?: return@run false
- bitmap.compress(format, params.quality, output)
+ val bitmap =
+ requireNotNull(BitmapFactory.decodeByteArray(data, 0, data.size, this)) {
+ "Failed to decode bitmap"
+ }
+ bitmap.compress(format, quality, output)
true
}
+ }
private fun BitmapFactory.Options.calculateInSampleSize(size: Int): Int {
var inSampleSize = 1
@@ -65,16 +72,3 @@ private class CompressingCoverFormat(
return inSampleSize
}
}
-
-private class AsIsCoverFormat : CoverFormat() {
- override val extension: String = "bin"
-
- override fun transcodeInto(data: ByteArray, output: OutputStream): Boolean {
- return try {
- output.write(data)
- true
- } catch (e: Exception) {
- false
- }
- }
-}