musikr: revamp cover system
Retains the stateless attributes of the older system but massively simplifies it compared to prior.
This commit is contained in:
parent
cd535eda2e
commit
879caf17db
12 changed files with 178 additions and 346 deletions
|
@ -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()
|
|
||||||
}
|
|
|
@ -19,28 +19,6 @@
|
||||||
package org.oxycblt.auxio.image.covers
|
package org.oxycblt.auxio.image.covers
|
||||||
|
|
||||||
import java.util.UUID
|
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? =
|
private fun String.toUuidOrNull(): UUID? =
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -18,20 +18,20 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.image.covers
|
package org.oxycblt.auxio.image.covers
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import org.oxycblt.musikr.covers.Cover
|
import org.oxycblt.musikr.covers.Cover
|
||||||
import org.oxycblt.musikr.covers.CoverResult
|
import org.oxycblt.musikr.covers.CoverResult
|
||||||
import org.oxycblt.musikr.covers.MutableCovers
|
import org.oxycblt.musikr.covers.MutableCovers
|
||||||
|
import org.oxycblt.musikr.covers.stored.CoverStorage
|
||||||
import org.oxycblt.musikr.fs.device.DeviceFile
|
import org.oxycblt.musikr.fs.device.DeviceFile
|
||||||
import org.oxycblt.musikr.metadata.Metadata
|
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 obtain(id: String) = CoverResult.Hit(NullCover)
|
||||||
|
|
||||||
override suspend fun create(file: DeviceFile, metadata: Metadata) = CoverResult.Hit(NullCover)
|
override suspend fun create(file: DeviceFile, metadata: Metadata) = CoverResult.Hit(NullCover)
|
||||||
|
|
||||||
override suspend fun cleanup(excluding: Collection<Cover>) {
|
override suspend fun cleanup(excluding: Collection<Cover>) {
|
||||||
context.coversDir().listFiles()?.forEach { it.deleteRecursively() }
|
storage.ls(setOf()).map { storage.rm(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Auxio Project
|
* Copyright (c) 2025 Auxio Project
|
||||||
* CoverUtil.kt is part of Auxio.
|
* RevisionedTranscoding.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,9 +18,9 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.image.covers
|
package org.oxycblt.auxio.image.covers
|
||||||
|
|
||||||
import android.content.Context
|
import java.util.UUID
|
||||||
import kotlinx.coroutines.Dispatchers
|
import org.oxycblt.musikr.covers.stored.Transcoding
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
suspend fun Context.coversDir() =
|
class RevisionedTranscoding(revision: UUID, private val inner: Transcoding) : Transcoding by inner {
|
||||||
withContext(Dispatchers.IO) { filesDir.resolve("covers").apply { mkdirs() } }
|
override val tag = "_$revision${inner.tag}"
|
||||||
|
}
|
|
@ -19,6 +19,7 @@
|
||||||
package org.oxycblt.auxio.image.covers
|
package org.oxycblt.auxio.image.covers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.image.CoverMode
|
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.FDCover
|
||||||
import org.oxycblt.musikr.covers.MutableCovers
|
import org.oxycblt.musikr.covers.MutableCovers
|
||||||
import org.oxycblt.musikr.covers.embedded.CoverIdentifier
|
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.FSCovers
|
||||||
import org.oxycblt.musikr.covers.fs.MutableFSCovers
|
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 {
|
interface SettingCovers {
|
||||||
suspend fun mutate(context: Context, revision: UUID): MutableCovers<out Cover>
|
suspend fun mutate(context: Context, revision: UUID): MutableCovers<out Cover>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun immutable(context: Context): Covers<FDCover> =
|
suspend fun immutable(context: Context): Covers<FDCover> =
|
||||||
Covers.chain(BaseSiloedCovers(context), FSCovers(context))
|
Covers.chain(StoredCovers(CoverStorage.at(context.coversDir())), FSCovers(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,17 +51,22 @@ class SettingCoversImpl
|
||||||
@Inject
|
@Inject
|
||||||
constructor(private val imageSettings: ImageSettings, private val identifier: CoverIdentifier) :
|
constructor(private val imageSettings: ImageSettings, private val identifier: CoverIdentifier) :
|
||||||
SettingCovers {
|
SettingCovers {
|
||||||
override suspend fun mutate(context: Context, revision: UUID): MutableCovers<out Cover> =
|
override suspend fun mutate(context: Context, revision: UUID): MutableCovers<out Cover> {
|
||||||
|
val coverStorage = CoverStorage.at(context.coversDir())
|
||||||
|
val transcoding =
|
||||||
when (imageSettings.coverMode) {
|
when (imageSettings.coverMode) {
|
||||||
CoverMode.OFF -> NullCovers(context)
|
CoverMode.OFF -> return NullCovers(coverStorage)
|
||||||
CoverMode.SAVE_SPACE -> siloedCovers(context, revision, CoverParams.of(500, 70))
|
CoverMode.SAVE_SPACE -> Compress(Bitmap.CompressFormat.JPEG, 500, 70)
|
||||||
CoverMode.BALANCED -> siloedCovers(context, revision, CoverParams.of(750, 85))
|
CoverMode.BALANCED -> Compress(Bitmap.CompressFormat.JPEG, 750, 85)
|
||||||
CoverMode.HIGH_QUALITY -> siloedCovers(context, revision, CoverParams.of(1000, 100))
|
CoverMode.HIGH_QUALITY -> Compress(Bitmap.CompressFormat.JPEG, 1000, 100)
|
||||||
CoverMode.AS_IS -> siloedCovers(context, revision, null)
|
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 suspend fun siloedCovers(context: Context, revision: UUID, with: CoverParams?) =
|
private fun Context.coversDir() = filesDir.resolve("covers")
|
||||||
MutableCovers.chain(
|
|
||||||
MutableSiloedCovers.from(context, CoverSilo(revision, with), identifier),
|
|
||||||
MutableFSCovers(context))
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -99,6 +99,10 @@ interface FDCover : Cover {
|
||||||
suspend fun fd(): ParcelFileDescriptor?
|
suspend fun fd(): ParcelFileDescriptor?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MemoryCover : Cover {
|
||||||
|
fun data(): ByteArray
|
||||||
|
}
|
||||||
|
|
||||||
class CoverCollection private constructor(val covers: List<Cover>) {
|
class CoverCollection private constructor(val covers: List<Cover>) {
|
||||||
override fun hashCode() = covers.hashCode()
|
override fun hashCode() = covers.hashCode()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,51 +18,36 @@
|
||||||
|
|
||||||
package org.oxycblt.musikr.covers.embedded
|
package org.oxycblt.musikr.covers.embedded
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import org.oxycblt.musikr.covers.Cover
|
import org.oxycblt.musikr.covers.Cover
|
||||||
import org.oxycblt.musikr.covers.CoverResult
|
import org.oxycblt.musikr.covers.CoverResult
|
||||||
import org.oxycblt.musikr.covers.Covers
|
import org.oxycblt.musikr.covers.MemoryCover
|
||||||
import org.oxycblt.musikr.covers.FDCover
|
|
||||||
import org.oxycblt.musikr.covers.MutableCovers
|
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.fs.device.DeviceFile
|
||||||
import org.oxycblt.musikr.metadata.Metadata
|
import org.oxycblt.musikr.metadata.Metadata
|
||||||
|
|
||||||
open class EmbeddedCovers(private val appFS: AppFS, private val coverFormat: CoverFormat) :
|
class EmbeddedCovers(private val coverIdentifier: CoverIdentifier) : MutableCovers<MemoryCover> {
|
||||||
Covers<FDCover> {
|
override suspend fun obtain(id: String): CoverResult<MemoryCover> = CoverResult.Miss()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun getFileName(id: String) = "$id.${coverFormat.extension}"
|
override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult<MemoryCover> {
|
||||||
}
|
|
||||||
|
|
||||||
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> {
|
|
||||||
val data = metadata.cover ?: return CoverResult.Miss()
|
val data = metadata.cover ?: return CoverResult.Miss()
|
||||||
val id = coverIdentifier.identify(data)
|
val id = coverIdentifier.identify(data)
|
||||||
val coverFile = appFS.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
|
return CoverResult.Hit(EmbeddedCover(id, data))
|
||||||
return CoverResult.Hit(InternalCoverImpl(id, coverFile))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun cleanup(excluding: Collection<Cover>) {
|
override suspend fun cleanup(excluding: Collection<Cover>) {}
|
||||||
val used = excluding.mapTo(mutableSetOf()) { getFileName(it.id) }
|
|
||||||
appFS.deleteWhere { it !in used }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class InternalCoverImpl(override val id: String, private val appFile: AppFile) :
|
private class EmbeddedCover(override val id: String, private val data: ByteArray) : MemoryCover {
|
||||||
FDCover {
|
override suspend fun open() = ByteArrayInputStream(data)
|
||||||
override suspend fun fd() = appFile.fd()
|
|
||||||
|
|
||||||
override suspend fun open() = appFile.open()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2024 Auxio Project
|
* 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
|
* 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
|
* 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/>.
|
* 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 android.os.ParcelFileDescriptor
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.oxycblt.musikr.covers.FDCover
|
||||||
|
|
||||||
interface AppFS {
|
interface CoverStorage {
|
||||||
suspend fun find(name: String): AppFile?
|
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 {
|
companion object {
|
||||||
suspend fun at(dir: File): AppFS {
|
suspend fun at(dir: File): CoverStorage {
|
||||||
withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) }
|
withContext(Dispatchers.IO) { check(dir.exists() && dir.isDirectory) }
|
||||||
return AppFSImpl(dir)
|
return CoverStorageImpl(dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppFile {
|
private class CoverStorageImpl(private val dir: File) : CoverStorage {
|
||||||
suspend fun fd(): ParcelFileDescriptor?
|
|
||||||
|
|
||||||
suspend fun open(): InputStream?
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AppFSImpl(private val dir: File) : AppFS {
|
|
||||||
private val fileMutexes = mutableMapOf<String, Mutex>()
|
private val fileMutexes = mutableMapOf<String, Mutex>()
|
||||||
private val mapMutex = Mutex()
|
private val mapMutex = Mutex()
|
||||||
|
|
||||||
|
@ -57,16 +53,16 @@ private class AppFSImpl(private val dir: File) : AppFS {
|
||||||
return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } }
|
return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun find(name: String): AppFile? =
|
override suspend fun find(name: String): FDCover? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
File(dir, name).takeIf { it.exists() }?.let { AppFileImpl(it) }
|
File(dir, name).takeIf { it.exists() }?.let { StoredCover(it) }
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
null
|
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)
|
val fileMutex = getMutexForFile(name)
|
||||||
return fileMutex.withLock {
|
return fileMutex.withLock {
|
||||||
val targetFile = File(dir, name)
|
val targetFile = File(dir, name)
|
||||||
|
@ -77,26 +73,31 @@ private class AppFSImpl(private val dir: File) : AppFS {
|
||||||
try {
|
try {
|
||||||
tempFile.outputStream().use { block(it) }
|
tempFile.outputStream().use { block(it) }
|
||||||
tempFile.renameTo(targetFile)
|
tempFile.renameTo(targetFile)
|
||||||
AppFileImpl(targetFile)
|
StoredCover(targetFile)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
tempFile.delete()
|
tempFile.delete()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
AppFileImpl(targetFile)
|
StoredCover(targetFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteWhere(block: (String) -> Boolean) {
|
override suspend fun ls(exclude: Set<String>): List<String> =
|
||||||
withContext(Dispatchers.IO) {
|
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() =
|
override suspend fun fd() =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
@ -107,4 +108,8 @@ private data class AppFileImpl(private val file: File) : AppFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun open() = withContext(Dispatchers.IO) { file.inputStream() }
|
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()
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Auxio Project
|
* 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
|
* 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
|
* 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/>.
|
* 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.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
abstract class CoverFormat {
|
interface Transcoding {
|
||||||
internal abstract val extension: String
|
val tag: String
|
||||||
|
|
||||||
internal abstract fun transcodeInto(data: ByteArray, output: OutputStream): Boolean
|
fun transcodeInto(data: ByteArray, output: OutputStream)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
object NoTranscoding : Transcoding {
|
||||||
fun jpeg(params: CoverParams): CoverFormat =
|
override val tag = ".img"
|
||||||
CompressingCoverFormat("jpg", params, Bitmap.CompressFormat.JPEG)
|
|
||||||
|
|
||||||
fun asIs(): CoverFormat = AsIsCoverFormat()
|
override fun transcodeInto(data: ByteArray, output: OutputStream) {
|
||||||
|
output.write(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CompressingCoverFormat(
|
class Compress(
|
||||||
override val extension: String,
|
|
||||||
private val params: CoverParams,
|
|
||||||
private val format: Bitmap.CompressFormat,
|
private val format: Bitmap.CompressFormat,
|
||||||
) : CoverFormat() {
|
private val resolution: Int,
|
||||||
override fun transcodeInto(data: ByteArray, output: OutputStream) =
|
private val quality: Int,
|
||||||
|
) : Transcoding {
|
||||||
|
override val tag = "_${resolution}x${quality}.${format.name.lowercase()}"
|
||||||
|
|
||||||
|
override fun transcodeInto(data: ByteArray, output: OutputStream) {
|
||||||
BitmapFactory.Options().run {
|
BitmapFactory.Options().run {
|
||||||
inJustDecodeBounds = true
|
inJustDecodeBounds = true
|
||||||
BitmapFactory.decodeByteArray(data, 0, data.size, this)
|
BitmapFactory.decodeByteArray(data, 0, data.size, this)
|
||||||
inSampleSize = calculateInSampleSize(params.resolution)
|
inSampleSize = calculateInSampleSize(resolution)
|
||||||
inJustDecodeBounds = false
|
inJustDecodeBounds = false
|
||||||
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, this) ?: return@run false
|
val bitmap =
|
||||||
bitmap.compress(format, params.quality, output)
|
requireNotNull(BitmapFactory.decodeByteArray(data, 0, data.size, this)) {
|
||||||
|
"Failed to decode bitmap"
|
||||||
|
}
|
||||||
|
bitmap.compress(format, quality, output)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun BitmapFactory.Options.calculateInSampleSize(size: Int): Int {
|
private fun BitmapFactory.Options.calculateInSampleSize(size: Int): Int {
|
||||||
var inSampleSize = 1
|
var inSampleSize = 1
|
||||||
|
@ -65,16 +72,3 @@ private class CompressingCoverFormat(
|
||||||
return inSampleSize
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue