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
|
import org.oxycblt.musikr.pipeline.RawSong
|
||||||
|
|
||||||
abstract class Cache {
|
abstract class Cache {
|
||||||
|
internal abstract fun lap(): Long
|
||||||
|
|
||||||
internal abstract suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult
|
internal abstract suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult
|
||||||
|
|
||||||
internal abstract suspend fun write(song: RawSong)
|
internal abstract suspend fun write(song: RawSong)
|
||||||
|
|
||||||
|
internal abstract suspend fun prune(timestamp: Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoredCache {
|
interface StoredCache {
|
||||||
|
@ -42,7 +46,7 @@ interface StoredCache {
|
||||||
internal sealed interface CacheResult {
|
internal sealed interface CacheResult {
|
||||||
data class Hit(val song: RawSong) : 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 {
|
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() {
|
private class CacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() {
|
||||||
|
override fun lap() = System.nanoTime()
|
||||||
|
|
||||||
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult {
|
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult {
|
||||||
val song = cacheInfoDao.selectSong(file.uri.toString()) ?:
|
val song = cacheInfoDao.selectSong(file.uri.toString()) ?:
|
||||||
return CacheResult.Miss(file, null)
|
return CacheResult.Miss(file, null)
|
||||||
if (song.dateModified != file.lastModified) {
|
if (song.modifiedMs != file.lastModified) {
|
||||||
return CacheResult.Miss(file, song.dateAdded)
|
// 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))
|
return CacheResult.Hit(song.intoRawSong(file, storedCovers))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun write(song: RawSong) =
|
override suspend fun write(song: RawSong) =
|
||||||
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
||||||
|
|
||||||
|
override suspend fun prune(timestamp: Long) {
|
||||||
|
cacheInfoDao.pruneOlderThan(timestamp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class WriteOnlyCacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() {
|
private class WriteOnlyCacheImpl(private val cacheInfoDao: CacheInfoDao) : Cache() {
|
||||||
|
override fun lap() = System.nanoTime()
|
||||||
|
|
||||||
override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) =
|
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) =
|
override suspend fun write(song: RawSong) =
|
||||||
cacheInfoDao.updateSong(CachedSong.fromRawSong(song))
|
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 android.content.Context
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
|
import androidx.room.Delete
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
|
@ -28,8 +29,10 @@ import androidx.room.PrimaryKey
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.Transaction
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
|
import androidx.room.Update
|
||||||
import org.oxycblt.musikr.cover.StoredCovers
|
import org.oxycblt.musikr.cover.StoredCovers
|
||||||
import org.oxycblt.musikr.fs.DeviceFile
|
import org.oxycblt.musikr.fs.DeviceFile
|
||||||
import org.oxycblt.musikr.metadata.Properties
|
import org.oxycblt.musikr.metadata.Properties
|
||||||
|
@ -57,18 +60,28 @@ internal interface CacheInfoDao {
|
||||||
@Query("SELECT * FROM CachedSong WHERE uri = :uri")
|
@Query("SELECT * FROM CachedSong WHERE uri = :uri")
|
||||||
suspend fun selectSong(uri: String): CachedSong?
|
suspend fun selectSong(uri: String): CachedSong?
|
||||||
|
|
||||||
@Query("SELECT dateAdded FROM CachedSong WHERE uri = :uri")
|
@Query("SELECT addedMs FROM CachedSong WHERE uri = :uri")
|
||||||
suspend fun selectDateAdded(uri: String): Long?
|
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)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
|
||||||
|
|
||||||
|
@Query("DELETE FROM CachedSong WHERE touchedNs < :now")
|
||||||
|
suspend fun pruneOlderThan(now: Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@TypeConverters(CachedSong.Converters::class)
|
@TypeConverters(CachedSong.Converters::class)
|
||||||
internal data class CachedSong(
|
internal data class CachedSong(
|
||||||
@PrimaryKey val uri: String,
|
@PrimaryKey val uri: String,
|
||||||
val dateModified: Long,
|
val modifiedMs: Long,
|
||||||
val dateAdded: Long,
|
val addedMs: Long,
|
||||||
|
val touchedNs: Long,
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
val durationMs: Long,
|
val durationMs: Long,
|
||||||
val bitrateHz: Int,
|
val bitrateHz: Int,
|
||||||
|
@ -122,7 +135,7 @@ internal data class CachedSong(
|
||||||
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
replayGainTrackAdjustment = replayGainTrackAdjustment,
|
||||||
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
|
replayGainAlbumAdjustment = replayGainAlbumAdjustment),
|
||||||
coverId?.let { storedCovers.obtain(it) },
|
coverId?.let { storedCovers.obtain(it) },
|
||||||
dateAdded = dateAdded)
|
addedMs = addedMs)
|
||||||
|
|
||||||
object Converters {
|
object Converters {
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
|
@ -141,8 +154,11 @@ internal data class CachedSong(
|
||||||
fun fromRawSong(rawSong: RawSong) =
|
fun fromRawSong(rawSong: RawSong) =
|
||||||
CachedSong(
|
CachedSong(
|
||||||
uri = rawSong.file.uri.toString(),
|
uri = rawSong.file.uri.toString(),
|
||||||
dateModified = rawSong.file.lastModified,
|
modifiedMs = rawSong.file.lastModified,
|
||||||
dateAdded = rawSong.dateAdded,
|
addedMs = rawSong.addedMs,
|
||||||
|
// Should be strictly monotonic so we don't prune this
|
||||||
|
// by accident later.
|
||||||
|
touchedNs = System.nanoTime(),
|
||||||
musicBrainzId = rawSong.tags.musicBrainzId,
|
musicBrainzId = rawSong.tags.musicBrainzId,
|
||||||
name = rawSong.tags.name,
|
name = rawSong.tags.name,
|
||||||
sortName = rawSong.tags.sortName,
|
sortName = rawSong.tags.sortName,
|
||||||
|
|
|
@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.mapNotNull
|
||||||
import kotlinx.coroutines.flow.merge
|
import kotlinx.coroutines.flow.merge
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.musikr.Storage
|
import org.oxycblt.musikr.Storage
|
||||||
import org.oxycblt.musikr.cache.Cache
|
import org.oxycblt.musikr.cache.Cache
|
||||||
|
@ -63,7 +64,8 @@ private class ExtractStepImpl(
|
||||||
private val storedCovers: MutableStoredCovers
|
private val storedCovers: MutableStoredCovers
|
||||||
) : ExtractStep {
|
) : ExtractStep {
|
||||||
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
override fun extract(nodes: Flow<ExploreNode>): Flow<ExtractedMusic> {
|
||||||
val startTime = System.currentTimeMillis()
|
val cacheTimestamp = cache.lap()
|
||||||
|
val addingMs = System.currentTimeMillis()
|
||||||
val filterFlow =
|
val filterFlow =
|
||||||
nodes.divert {
|
nodes.divert {
|
||||||
when (it) {
|
when (it) {
|
||||||
|
@ -128,7 +130,7 @@ private class ExtractStepImpl(
|
||||||
.mapNotNull { fileWith ->
|
.mapNotNull { fileWith ->
|
||||||
val tags = tagParser.parse(fileWith.file, fileWith.with)
|
val tags = tagParser.parse(fileWith.file, fileWith.with)
|
||||||
val cover = fileWith.with.cover?.let { storedCovers.write(it) }
|
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)
|
.flowOn(Dispatchers.IO)
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
|
@ -146,6 +148,9 @@ private class ExtractStepImpl(
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
}
|
}
|
||||||
.flattenMerge()
|
.flattenMerge()
|
||||||
|
.onCompletion {
|
||||||
|
cache.prune(cacheTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
return merge(
|
return merge(
|
||||||
filterFlow.manager,
|
filterFlow.manager,
|
||||||
|
@ -165,7 +170,7 @@ internal data class RawSong(
|
||||||
val properties: Properties,
|
val properties: Properties,
|
||||||
val tags: ParsedTags,
|
val tags: ParsedTags,
|
||||||
val cover: Cover?,
|
val cover: Cover?,
|
||||||
val dateAdded: Long
|
val addedMs: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
internal sealed interface ExtractedMusic {
|
internal sealed interface ExtractedMusic {
|
||||||
|
|
|
@ -65,7 +65,7 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T
|
||||||
size = song.file.size,
|
size = song.file.size,
|
||||||
format = Format.infer(song.file.mimeType, song.properties.mimeType),
|
format = Format.infer(song.file.mimeType, song.properties.mimeType),
|
||||||
lastModified = song.file.lastModified,
|
lastModified = song.file.lastModified,
|
||||||
dateAdded = song.dateAdded,
|
dateAdded = song.addedMs,
|
||||||
musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(),
|
musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(),
|
||||||
name = interpretation.naming.name(song.tags.name, song.tags.sortName),
|
name = interpretation.naming.name(song.tags.name, song.tags.sortName),
|
||||||
rawName = song.tags.name,
|
rawName = song.tags.name,
|
||||||
|
|
Loading…
Reference in a new issue