musikr: add cache pruning
Helps remove dead entries, and additionally makes date added values more accurate over time.s
This commit is contained in:
parent
4f920e922d
commit
3f364dc5c6
4 changed files with 57 additions and 15 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue