musikr: add cache pruning

Helps remove dead entries, and additionally makes date added values more
accurate over time.s
This commit is contained in:
Alexander Capehart 2024-12-26 13:36:56 -05:00
parent 4f920e922d
commit 3f364dc5c6
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 57 additions and 15 deletions

View file

@ -24,9 +24,13 @@ import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.pipeline.RawSong
abstract class Cache {
internal abstract fun lap(): Long
internal abstract suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult
internal abstract suspend fun write(song: RawSong)
internal abstract suspend fun prune(timestamp: Long)
}
interface StoredCache {
@ -42,7 +46,7 @@ interface StoredCache {
internal sealed interface CacheResult {
data class Hit(val song: RawSong) : CacheResult
data class Miss(val file: DeviceFile, val dateAdded: Long?) : CacheResult
data class Miss(val file: DeviceFile, val addedMs: Long?) : CacheResult
}
private class StoredCacheImpl(private val database: CacheDatabase) : StoredCache {
@ -52,23 +56,40 @@ private class StoredCacheImpl(private val database: CacheDatabase) : StoredCache
}
private class CacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() {
override fun lap() = System.nanoTime()
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult {
val song = cacheInfoDao.selectSong(file.uri.toString()) ?:
return CacheResult.Miss(file, null)
if (song.dateModified != file.lastModified) {
return CacheResult.Miss(file, song.dateAdded)
if (song.modifiedMs != file.lastModified) {
// We *found* this file earlier, but it's out of date.
// Send back it with the timestamp so it will be re-used.
// The touch timestamp will be updated on write.
return CacheResult.Miss(file, song.addedMs)
}
// Valid file, update the touch time.
cacheInfoDao.touch(file.uri.toString())
return CacheResult.Hit(song.intoRawSong(file, storedCovers))
}
override suspend fun write(song: RawSong) =
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
override suspend fun prune(timestamp: Long) {
cacheInfoDao.pruneOlderThan(timestamp)
}
}
private class WriteOnlyCacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() {
override fun lap() = System.nanoTime()
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) =
CacheResult.Miss(file, cacheInfoDao.selectDateAdded(file.uri.toString()))
CacheResult.Miss(file, cacheInfoDao.selectAddedMs(file.uri.toString()))
override suspend fun write(song: RawSong) =
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
override suspend fun prune(timestamp: Long) {
cacheInfoDao.pruneOlderThan(timestamp)
}
}

View file

@ -21,6 +21,7 @@ package org.oxycblt.musikr.cache
import android.content.Context
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Delete
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
@ -28,8 +29,10 @@ import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.Transaction
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import androidx.room.Update
import org.oxycblt.musikr.cover.StoredCovers
import org.oxycblt.musikr.fs.DeviceFile
import org.oxycblt.musikr.metadata.Properties
@ -57,18 +60,28 @@ internal interface CacheInfoDao {
@Query("SELECT * FROM CachedSong WHERE uri = :uri")
suspend fun selectSong(uri: String): CachedSong?
@Query("SELECT dateAdded FROM CachedSong WHERE uri = :uri")
suspend fun selectDateAdded(uri: String): Long?
@Query("SELECT addedMs FROM CachedSong WHERE uri = :uri")
suspend fun selectAddedMs(uri: String): Long?
@Transaction
suspend fun touch(uri: String) = updateTouchedNs(uri, System.nanoTime())
@Query("UPDATE cachedsong SET touchedNs = :nowNs WHERE uri = :uri")
suspend fun updateTouchedNs(uri: String, nowNs: Long)
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
@Query("DELETE FROM CachedSong WHERE touchedNs < :now")
suspend fun pruneOlderThan(now: Long)
}
@Entity
@TypeConverters(CachedSong.Converters::class)
internal data class CachedSong(
@PrimaryKey val uri: String,
val dateModified: Long,
val dateAdded: Long,
val modifiedMs: Long,
val addedMs: Long,
val touchedNs: Long,
val mimeType: String,
val durationMs: Long,
val bitrateHz: Int,
@ -122,7 +135,7 @@ internal data class CachedSong(
replayGainTrackAdjustment = replayGainTrackAdjustment,
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
coverId?.let { storedCovers.obtain(it) },
dateAdded = dateAdded)
addedMs = addedMs)
object Converters {
@TypeConverter
@ -141,8 +154,11 @@ internal data class CachedSong(
fun fromRawSong(rawSong: RawSong) =
CachedSong(
uri = rawSong.file.uri.toString(),
dateModified = rawSong.file.lastModified,
dateAdded = rawSong.dateAdded,
modifiedMs = rawSong.file.lastModified,
addedMs = rawSong.addedMs,
// Should be strictly monotonic so we don't prune this
// by accident later.
touchedNs = System.nanoTime(),
musicBrainzId = rawSong.tags.musicBrainzId,
name = rawSong.tags.name,
sortName = rawSong.tags.sortName,

View file

@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.withContext
import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.Cache
@ -63,7 +64,8 @@ private class ExtractStepImpl(
private val storedCovers: MutableStoredCovers
) : ExtractStep {
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
val startTime = System.currentTimeMillis()
val cacheTimestamp = cache.lap()
val addingMs = System.currentTimeMillis()
val filterFlow =
nodes.divert {
when (it) {
@ -128,7 +130,7 @@ private class ExtractStepImpl(
.mapNotNull { fileWith ->
val tags = tagParser.parse(fileWith.file, fileWith.with)
val cover = fileWith.with.cover?.let { storedCovers.write(it) }
RawSong(fileWith.file, fileWith.with.properties, tags, cover, startTime)
RawSong(fileWith.file, fileWith.with.properties, tags, cover, addingMs)
}
.flowOn(Dispatchers.IO)
.buffer(Channel.UNLIMITED)
@ -146,6 +148,9 @@ private class ExtractStepImpl(
.buffer(Channel.UNLIMITED)
}
.flattenMerge()
.onCompletion {
cache.prune(cacheTimestamp)
}
return merge(
filterFlow.manager,
@ -165,7 +170,7 @@ internal data class RawSong(
val properties: Properties,
val tags: ParsedTags,
val cover: Cover?,
val dateAdded: Long
val addedMs: Long
)
internal sealed interface ExtractedMusic {

View file

@ -65,7 +65,7 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
size = song.file.size,
format = Format.infer(song.file.mimeType, song.properties.mimeType),
lastModified = song.file.lastModified,
dateAdded = song.dateAdded,
dateAdded = song.addedMs,
musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(),
name = interpretation.naming.name(song.tags.name, song.tags.sortName),
rawName = song.tags.name,