musikr: handle missing covers on recaching
Now that we have effectively two caches (The main cache and the covers), we have to handle the case where we have cached data, but the cover data is missing. This is a real-world edge case once album covers are made configurable as they were previously.
This commit is contained in:
parent
32156f23b2
commit
8b3d7cae9c
8 changed files with 68 additions and 43 deletions
|
@ -28,23 +28,25 @@ import org.oxycblt.musikr.cover.Cover
|
|||
import org.oxycblt.musikr.cover.CoverFiles
|
||||
import org.oxycblt.musikr.cover.CoverFormat
|
||||
import org.oxycblt.musikr.cover.CoverParams
|
||||
import org.oxycblt.musikr.cover.MutableStoredCovers
|
||||
import org.oxycblt.musikr.cover.StoredCovers
|
||||
import org.oxycblt.musikr.cover.Covers
|
||||
import org.oxycblt.musikr.cover.MutableCovers
|
||||
import org.oxycblt.musikr.cover.ObtainResult
|
||||
|
||||
class SiloedCovers(
|
||||
private val rootDir: File,
|
||||
private val silo: CoverSilo,
|
||||
private val inner: MutableStoredCovers
|
||||
) : MutableStoredCovers {
|
||||
override suspend fun obtain(id: String): SiloedCover? {
|
||||
val coverId = SiloedCoverId.parse(id) ?: return null
|
||||
if (coverId.silo != silo) return null
|
||||
return inner.obtain(coverId.id)?.let { SiloedCover(coverId.silo, it) }
|
||||
private val inner: MutableCovers
|
||||
) : MutableCovers {
|
||||
override suspend fun obtain(id: String): ObtainResult {
|
||||
val coverId = SiloedCoverId.parse(id) ?: return ObtainResult.Miss
|
||||
if (coverId.silo != silo) return ObtainResult.Miss
|
||||
return when (val result = inner.obtain(coverId.id)) {
|
||||
is ObtainResult.Hit -> ObtainResult.Hit(SiloedCover(silo, result.cover))
|
||||
is ObtainResult.Miss -> ObtainResult.Miss
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun write(data: ByteArray): SiloedCover? {
|
||||
return inner.write(data)?.let { SiloedCover(silo, it) }
|
||||
}
|
||||
override suspend fun write(data: ByteArray) = SiloedCover(silo, inner.write(data))
|
||||
|
||||
override suspend fun cleanup(assuming: Library) {
|
||||
inner.cleanup(assuming)
|
||||
|
@ -66,7 +68,7 @@ class SiloedCovers(
|
|||
}
|
||||
val files = CoverFiles.at(revisionDir)
|
||||
val format = CoverFormat.jpeg(silo.params)
|
||||
return SiloedCovers(rootDir, silo, StoredCovers.from(files, format))
|
||||
return SiloedCovers(rootDir, silo, Covers.from(files, format))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,14 +19,14 @@
|
|||
package org.oxycblt.musikr
|
||||
|
||||
import org.oxycblt.musikr.cache.Cache
|
||||
import org.oxycblt.musikr.cover.MutableStoredCovers
|
||||
import org.oxycblt.musikr.cover.MutableCovers
|
||||
import org.oxycblt.musikr.playlist.db.StoredPlaylists
|
||||
import org.oxycblt.musikr.tag.interpret.Naming
|
||||
import org.oxycblt.musikr.tag.interpret.Separators
|
||||
|
||||
data class Storage(
|
||||
val cache: Cache.Factory,
|
||||
val storedCovers: MutableStoredCovers,
|
||||
val storedCovers: MutableCovers,
|
||||
val storedPlaylists: StoredPlaylists
|
||||
)
|
||||
|
||||
|
|
|
@ -18,12 +18,12 @@
|
|||
|
||||
package org.oxycblt.musikr.cache
|
||||
|
||||
import org.oxycblt.musikr.cover.StoredCovers
|
||||
import org.oxycblt.musikr.cover.Covers
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.pipeline.RawSong
|
||||
|
||||
abstract class Cache {
|
||||
internal abstract suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult
|
||||
internal abstract suspend fun read(file: DeviceFile, covers: Covers): CacheResult
|
||||
|
||||
internal abstract suspend fun write(song: RawSong)
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ import androidx.room.RoomDatabase
|
|||
import androidx.room.Transaction
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import org.oxycblt.musikr.cover.StoredCovers
|
||||
import org.oxycblt.musikr.cover.Covers
|
||||
import org.oxycblt.musikr.cover.ObtainResult
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.metadata.Properties
|
||||
import org.oxycblt.musikr.pipeline.RawSong
|
||||
|
@ -117,8 +118,17 @@ internal data class CachedSong(
|
|||
val replayGainAlbumAdjustment: Float?,
|
||||
val coverId: String?,
|
||||
) {
|
||||
suspend fun intoRawSong(file: DeviceFile, storedCovers: StoredCovers) =
|
||||
RawSong(
|
||||
suspend fun intoRawSong(file: DeviceFile, covers: Covers): RawSong? {
|
||||
val cover =
|
||||
when (val result = coverId?.let { covers.obtain(it) }) {
|
||||
// We found the cover.
|
||||
is ObtainResult.Hit -> result.cover
|
||||
// We actually didn't find the cover, can't safely convert.
|
||||
is ObtainResult.Miss -> return null
|
||||
// No cover in the first place, can ignore.
|
||||
null -> null
|
||||
}
|
||||
return RawSong(
|
||||
file,
|
||||
Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
|
||||
ParsedTags(
|
||||
|
@ -143,8 +153,9 @@ internal data class CachedSong(
|
|||
genreNames = genreNames,
|
||||
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
||||
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
|
||||
coverId?.let { storedCovers.obtain(it) },
|
||||
cover = cover,
|
||||
addedMs = addedMs)
|
||||
}
|
||||
|
||||
object Converters {
|
||||
@TypeConverter
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
package org.oxycblt.musikr.cache
|
||||
|
||||
import android.content.Context
|
||||
import org.oxycblt.musikr.cover.StoredCovers
|
||||
import org.oxycblt.musikr.cover.Covers
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.pipeline.RawSong
|
||||
|
||||
|
@ -53,7 +53,7 @@ private abstract class BaseStoredCache(protected val writeDao: CacheWriteDao) :
|
|||
|
||||
private class VisibleStoredCache(private val visibleDao: VisibleCacheDao, writeDao: CacheWriteDao) :
|
||||
BaseStoredCache(writeDao) {
|
||||
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult {
|
||||
override suspend fun read(file: DeviceFile, covers: Covers): CacheResult {
|
||||
val song = visibleDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file, null)
|
||||
if (song.modifiedMs != file.lastModified) {
|
||||
// We *found* this file earlier, but it's out of date.
|
||||
|
@ -63,7 +63,8 @@ private class VisibleStoredCache(private val visibleDao: VisibleCacheDao, writeD
|
|||
}
|
||||
// Valid file, update the touch time.
|
||||
visibleDao.touch(file.uri.toString())
|
||||
return CacheResult.Hit(song.intoRawSong(file, storedCovers))
|
||||
val rawSong = song.intoRawSong(file, covers) ?: return CacheResult.Miss(file, song.addedMs)
|
||||
return CacheResult.Hit(rawSong)
|
||||
}
|
||||
|
||||
class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
|
||||
|
@ -76,7 +77,7 @@ private class InvisibleStoredCache(
|
|||
private val invisibleCacheDao: InvisibleCacheDao,
|
||||
writeDao: CacheWriteDao
|
||||
) : BaseStoredCache(writeDao) {
|
||||
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) =
|
||||
override suspend fun read(file: DeviceFile, covers: Covers) =
|
||||
CacheResult.Miss(file, invisibleCacheDao.selectAddedMs(file.uri.toString()))
|
||||
|
||||
class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
|
||||
|
|
|
@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext
|
|||
interface CoverFiles {
|
||||
suspend fun find(name: String): CoverFile?
|
||||
|
||||
suspend fun write(name: String, block: suspend (OutputStream) -> Unit): CoverFile?
|
||||
suspend fun write(name: String, block: suspend (OutputStream) -> Unit): CoverFile
|
||||
|
||||
suspend fun deleteWhere(block: (String) -> Boolean)
|
||||
|
||||
|
@ -63,7 +63,7 @@ private class CoverFilesImpl(private val dir: File) : CoverFiles {
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun write(name: String, block: suspend (OutputStream) -> Unit): CoverFile? {
|
||||
override suspend fun write(name: String, block: suspend (OutputStream) -> Unit): CoverFile {
|
||||
val fileMutex = getMutexForFile(name)
|
||||
return fileMutex.withLock {
|
||||
val targetFile = File(dir, name)
|
||||
|
@ -77,7 +77,7 @@ private class CoverFilesImpl(private val dir: File) : CoverFiles {
|
|||
CoverFileImpl(targetFile)
|
||||
} catch (e: IOException) {
|
||||
tempFile.delete()
|
||||
null
|
||||
throw e
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Auxio Project
|
||||
* StoredCovers.kt is part of Auxio.
|
||||
* Covers.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
|
||||
|
@ -20,37 +20,48 @@ package org.oxycblt.musikr.cover
|
|||
|
||||
import org.oxycblt.musikr.Library
|
||||
|
||||
interface StoredCovers {
|
||||
suspend fun obtain(id: String): Cover?
|
||||
interface Covers {
|
||||
suspend fun obtain(id: String): ObtainResult
|
||||
|
||||
companion object {
|
||||
fun from(
|
||||
coverFiles: CoverFiles,
|
||||
coverFormat: CoverFormat,
|
||||
identifier: CoverIdentifier = CoverIdentifier.md5()
|
||||
): MutableStoredCovers = FileStoredCovers(coverFiles, coverFormat, identifier)
|
||||
): MutableCovers = FileCovers(coverFiles, coverFormat, identifier)
|
||||
}
|
||||
}
|
||||
|
||||
interface MutableStoredCovers : StoredCovers {
|
||||
suspend fun write(data: ByteArray): Cover?
|
||||
interface MutableCovers : Covers {
|
||||
suspend fun write(data: ByteArray): Cover
|
||||
|
||||
suspend fun cleanup(assuming: Library)
|
||||
}
|
||||
|
||||
private class FileStoredCovers(
|
||||
sealed interface ObtainResult {
|
||||
data class Hit(val cover: Cover) : ObtainResult
|
||||
|
||||
data object Miss : ObtainResult
|
||||
}
|
||||
|
||||
private class FileCovers(
|
||||
private val coverFiles: CoverFiles,
|
||||
private val coverFormat: CoverFormat,
|
||||
private val coverIdentifier: CoverIdentifier,
|
||||
) : StoredCovers, MutableStoredCovers {
|
||||
override suspend fun obtain(id: String) =
|
||||
coverFiles.find(getFileName(id))?.let { FileCover(id, it) }
|
||||
) : Covers, MutableCovers {
|
||||
override suspend fun obtain(id: String): ObtainResult {
|
||||
val file = coverFiles.find(getFileName(id))
|
||||
return if (file != null) {
|
||||
ObtainResult.Hit(FileCover(id, file))
|
||||
} else {
|
||||
ObtainResult.Miss
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun write(data: ByteArray): Cover? {
|
||||
override suspend fun write(data: ByteArray): Cover {
|
||||
val id = coverIdentifier.identify(data)
|
||||
return coverFiles
|
||||
.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
|
||||
?.let { FileCover(id, it) }
|
||||
val file = coverFiles.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
|
||||
return FileCover(id, file)
|
||||
}
|
||||
|
||||
override suspend fun cleanup(assuming: Library) {
|
|
@ -35,7 +35,7 @@ import org.oxycblt.musikr.Storage
|
|||
import org.oxycblt.musikr.cache.Cache
|
||||
import org.oxycblt.musikr.cache.CacheResult
|
||||
import org.oxycblt.musikr.cover.Cover
|
||||
import org.oxycblt.musikr.cover.MutableStoredCovers
|
||||
import org.oxycblt.musikr.cover.MutableCovers
|
||||
import org.oxycblt.musikr.fs.DeviceFile
|
||||
import org.oxycblt.musikr.metadata.MetadataExtractor
|
||||
import org.oxycblt.musikr.metadata.Properties
|
||||
|
@ -62,7 +62,7 @@ private class ExtractStepImpl(
|
|||
private val metadataExtractor: MetadataExtractor,
|
||||
private val tagParser: TagParser,
|
||||
private val cacheFactory: Cache.Factory,
|
||||
private val storedCovers: MutableStoredCovers
|
||||
private val storedCovers: MutableCovers
|
||||
) : ExtractStep {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
||||
|
|
Loading…
Reference in a new issue