musikr: revamp cover system

Retains the stateless attributes of the older system but massively
simplifies it compared to prior.
This commit is contained in:
Alexander Capehart 2025-03-08 14:53:43 -07:00
parent cd535eda2e
commit 879caf17db
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 178 additions and 346 deletions

View file

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

View file

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

View file

@ -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<NullCover> {
class NullCovers(private val storage: CoverStorage) : MutableCovers<NullCover> {
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<Cover>) {
context.coversDir().listFiles()?.forEach { it.deleteRecursively() }
storage.ls(setOf()).map { storage.rm(it) }
}
}

View file

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

View file

@ -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<out Cover>
companion object {
fun immutable(context: Context): Covers<FDCover> =
Covers.chain(BaseSiloedCovers(context), FSCovers(context))
suspend fun immutable(context: Context): Covers<FDCover> =
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<out Cover> =
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<out Cover> {
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")

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<FDCover> {
override suspend fun obtain(id: String): CoverResult<FDCover> {
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<FDCover> {
override suspend fun obtain(id: String): CoverResult<FDCover> {
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<FDCover> {
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<FDCover> =
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<Cover>) {
fileCovers.cleanup(excluding.filterIsInstance<SiloedCover>().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)
}
}
}

View file

@ -99,6 +99,10 @@ interface FDCover : Cover {
suspend fun fd(): ParcelFileDescriptor?
}
interface MemoryCover : Cover {
fun data(): ByteArray
}
class CoverCollection private constructor(val covers: List<Cover>) {
override fun hashCode() = covers.hashCode()

View file

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

View file

@ -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<FDCover> {
override suspend fun obtain(id: String): CoverResult<FDCover> {
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<MemoryCover> {
override suspend fun obtain(id: String): CoverResult<MemoryCover> = 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<FDCover> {
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<FDCover> {
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<MemoryCover> {
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<Cover>) {
val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) }
appFS.deleteWhere { it !in used }
override suspend fun cleanup(excluding: Collection<Cover>) {}
}
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()
}

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<FDCover> {
override suspend fun obtain(id: String): CoverResult<FDCover> {
val cover = coverStorage.find(id) ?: return CoverResult.Miss()
return CoverResult.Hit(cover)
}
}
class MutableStoredCovers(
private val src: MutableCovers<MemoryCover>,
private val coverStorage: CoverStorage,
private val transcoding: Transcoding
) : MutableCovers<FDCover> {
private val base = StoredCovers(coverStorage)
override suspend fun obtain(id: String): CoverResult<FDCover> = base.obtain(id)
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<FDCover> {
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<Cover>) {
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)
}
}
}

View file

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