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.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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
|
@ -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> {
|
||||||
|
|
Loading…
Reference in a new issue