diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt index 758640a16..cfd9f9511 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt @@ -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) + } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index 45f5dcf97..96f50b03b 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -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, diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index 25e90344e..2e6890128 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -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): Flow { - 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 { diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt index 618272daa..e4eb3ea29 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt @@ -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,