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:
Alexander Capehart 2024-12-27 15:11:09 -05:00
parent 32156f23b2
commit 8b3d7cae9c
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 68 additions and 43 deletions

View file

@ -28,23 +28,25 @@ import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.cover.CoverFiles import org.oxycblt.musikr.cover.CoverFiles
import org.oxycblt.musikr.cover.CoverFormat import org.oxycblt.musikr.cover.CoverFormat
import org.oxycblt.musikr.cover.CoverParams import org.oxycblt.musikr.cover.CoverParams
import org.oxycblt.musikr.cover.MutableStoredCovers import org.oxycblt.musikr.cover.Covers
import org.oxycblt.musikr.cover.StoredCovers import org.oxycblt.musikr.cover.MutableCovers
import org.oxycblt.musikr.cover.ObtainResult
class SiloedCovers( class SiloedCovers(
private val rootDir: File, private val rootDir: File,
private val silo: CoverSilo, private val silo: CoverSilo,
private val inner: MutableStoredCovers private val inner: MutableCovers
) : MutableStoredCovers { ) : MutableCovers {
override suspend fun obtain(id: String): SiloedCover? { override suspend fun obtain(id: String): ObtainResult {
val coverId = SiloedCoverId.parse(id) ?: return null val coverId = SiloedCoverId.parse(id) ?: return ObtainResult.Miss
if (coverId.silo != silo) return null if (coverId.silo != silo) return ObtainResult.Miss
return inner.obtain(coverId.id)?.let { SiloedCover(coverId.silo, it) } 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? { override suspend fun write(data: ByteArray) = SiloedCover(silo, inner.write(data))
return inner.write(data)?.let { SiloedCover(silo, it) }
}
override suspend fun cleanup(assuming: Library) { override suspend fun cleanup(assuming: Library) {
inner.cleanup(assuming) inner.cleanup(assuming)
@ -66,7 +68,7 @@ class SiloedCovers(
} }
val files = CoverFiles.at(revisionDir) val files = CoverFiles.at(revisionDir)
val format = CoverFormat.jpeg(silo.params) val format = CoverFormat.jpeg(silo.params)
return SiloedCovers(rootDir, silo, StoredCovers.from(files, format)) return SiloedCovers(rootDir, silo, Covers.from(files, format))
} }
} }
} }

View file

@ -19,14 +19,14 @@
package org.oxycblt.musikr package org.oxycblt.musikr
import org.oxycblt.musikr.cache.Cache 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.playlist.db.StoredPlaylists
import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Naming
import org.oxycblt.musikr.tag.interpret.Separators import org.oxycblt.musikr.tag.interpret.Separators
data class Storage( data class Storage(
val cache: Cache.Factory, val cache: Cache.Factory,
val storedCovers: MutableStoredCovers, val storedCovers: MutableCovers,
val storedPlaylists: StoredPlaylists val storedPlaylists: StoredPlaylists
) )

View file

@ -18,12 +18,12 @@
package org.oxycblt.musikr.cache 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.fs.DeviceFile
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
abstract class Cache { 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) internal abstract suspend fun write(song: RawSong)

View file

@ -31,7 +31,8 @@ import androidx.room.RoomDatabase
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.TypeConverter import androidx.room.TypeConverter
import androidx.room.TypeConverters 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.fs.DeviceFile
import org.oxycblt.musikr.metadata.Properties import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
@ -117,8 +118,17 @@ internal data class CachedSong(
val replayGainAlbumAdjustment: Float?, val replayGainAlbumAdjustment: Float?,
val coverId: String?, val coverId: String?,
) { ) {
suspend fun intoRawSong(file: DeviceFile, storedCovers: StoredCovers) = suspend fun intoRawSong(file: DeviceFile, covers: Covers): RawSong? {
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, file,
Properties(mimeType, durationMs, bitrateHz, sampleRateHz), Properties(mimeType, durationMs, bitrateHz, sampleRateHz),
ParsedTags( ParsedTags(
@ -143,8 +153,9 @@ internal data class CachedSong(
genreNames = genreNames, genreNames = genreNames,
replayGainTrackAdjustment = replayGainTrackAdjustment, replayGainTrackAdjustment = replayGainTrackAdjustment,
replayGainAlbumAdjustment = replayGainAlbumAdjustment), replayGainAlbumAdjustment = replayGainAlbumAdjustment),
coverId?.let { storedCovers.obtain(it) }, cover = cover,
addedMs = addedMs) addedMs = addedMs)
}
object Converters { object Converters {
@TypeConverter @TypeConverter

View file

@ -19,7 +19,7 @@
package org.oxycblt.musikr.cache package org.oxycblt.musikr.cache
import android.content.Context 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.fs.DeviceFile
import org.oxycblt.musikr.pipeline.RawSong 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) : private class VisibleStoredCache(private val visibleDao: VisibleCacheDao, writeDao: CacheWriteDao) :
BaseStoredCache(writeDao) { 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) val song = visibleDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file, null)
if (song.modifiedMs != file.lastModified) { if (song.modifiedMs != file.lastModified) {
// We *found* this file earlier, but it's out of date. // 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. // Valid file, update the touch time.
visibleDao.touch(file.uri.toString()) 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() { class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {
@ -76,7 +77,7 @@ private class InvisibleStoredCache(
private val invisibleCacheDao: InvisibleCacheDao, private val invisibleCacheDao: InvisibleCacheDao,
writeDao: CacheWriteDao writeDao: CacheWriteDao
) : BaseStoredCache(writeDao) { ) : 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())) CacheResult.Miss(file, invisibleCacheDao.selectAddedMs(file.uri.toString()))
class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() { class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() {

View file

@ -30,7 +30,7 @@ import kotlinx.coroutines.withContext
interface CoverFiles { interface CoverFiles {
suspend fun find(name: String): CoverFile? 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) 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) val fileMutex = getMutexForFile(name)
return fileMutex.withLock { return fileMutex.withLock {
val targetFile = File(dir, name) val targetFile = File(dir, name)
@ -77,7 +77,7 @@ private class CoverFilesImpl(private val dir: File) : CoverFiles {
CoverFileImpl(targetFile) CoverFileImpl(targetFile)
} catch (e: IOException) { } catch (e: IOException) {
tempFile.delete() tempFile.delete()
null throw e
} }
} }
} else { } else {

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (c) 2024 Auxio Project * 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 * 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
@ -20,37 +20,48 @@ package org.oxycblt.musikr.cover
import org.oxycblt.musikr.Library import org.oxycblt.musikr.Library
interface StoredCovers { interface Covers {
suspend fun obtain(id: String): Cover? suspend fun obtain(id: String): ObtainResult
companion object { companion object {
fun from( fun from(
coverFiles: CoverFiles, coverFiles: CoverFiles,
coverFormat: CoverFormat, coverFormat: CoverFormat,
identifier: CoverIdentifier = CoverIdentifier.md5() identifier: CoverIdentifier = CoverIdentifier.md5()
): MutableStoredCovers = FileStoredCovers(coverFiles, coverFormat, identifier) ): MutableCovers = FileCovers(coverFiles, coverFormat, identifier)
} }
} }
interface MutableStoredCovers : StoredCovers { interface MutableCovers : Covers {
suspend fun write(data: ByteArray): Cover? suspend fun write(data: ByteArray): Cover
suspend fun cleanup(assuming: Library) 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 coverFiles: CoverFiles,
private val coverFormat: CoverFormat, private val coverFormat: CoverFormat,
private val coverIdentifier: CoverIdentifier, private val coverIdentifier: CoverIdentifier,
) : StoredCovers, MutableStoredCovers { ) : Covers, MutableCovers {
override suspend fun obtain(id: String) = override suspend fun obtain(id: String): ObtainResult {
coverFiles.find(getFileName(id))?.let { FileCover(id, it) } 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) val id = coverIdentifier.identify(data)
return coverFiles val file = coverFiles.write(getFileName(id)) { coverFormat.transcodeInto(data, it) }
.write(getFileName(id)) { coverFormat.transcodeInto(data, it) } return FileCover(id, file)
?.let { FileCover(id, it) }
} }
override suspend fun cleanup(assuming: Library) { override suspend fun cleanup(assuming: Library) {

View file

@ -35,7 +35,7 @@ import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.Cache import org.oxycblt.musikr.cache.Cache
import org.oxycblt.musikr.cache.CacheResult import org.oxycblt.musikr.cache.CacheResult
import org.oxycblt.musikr.cover.Cover 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.fs.DeviceFile
import org.oxycblt.musikr.metadata.MetadataExtractor import org.oxycblt.musikr.metadata.MetadataExtractor
import org.oxycblt.musikr.metadata.Properties import org.oxycblt.musikr.metadata.Properties
@ -62,7 +62,7 @@ private class ExtractStepImpl(
private val metadataExtractor: MetadataExtractor, private val metadataExtractor: MetadataExtractor,
private val tagParser: TagParser, private val tagParser: TagParser,
private val cacheFactory: Cache.Factory, private val cacheFactory: Cache.Factory,
private val storedCovers: MutableStoredCovers private val storedCovers: MutableCovers
) : ExtractStep { ) : ExtractStep {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> { override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {