diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 25b8f65a6..b07fccca6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -362,7 +362,7 @@ constructor( val currentRevision = musicSettings.revision val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID() - val cache = if (withCache) storedCache.full() else storedCache.writeOnly() + val cache = if (withCache) storedCache.visible() else storedCache.invisible() val covers = MutableRevisionedStoredCovers(context, newRevision) val storage = Storage(cache, covers, storedPlaylists) val interpretation = Interpretation(nameFactory, separators) diff --git a/musikr/src/main/java/org/oxycblt/musikr/Config.kt b/musikr/src/main/java/org/oxycblt/musikr/Config.kt index 22ec833dd..c3d720847 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/Config.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/Config.kt @@ -25,7 +25,7 @@ import org.oxycblt.musikr.tag.interpret.Naming import org.oxycblt.musikr.tag.interpret.Separators data class Storage( - val cache: Cache, + val cache: Cache.Factory, val storedCovers: MutableStoredCovers, val storedPlaylists: StoredPlaylists ) 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 cfd9f9511..94d37726e 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/Cache.kt @@ -18,28 +18,19 @@ package org.oxycblt.musikr.cache -import android.content.Context import org.oxycblt.musikr.cover.StoredCovers 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) -} + internal abstract suspend fun finalize() -interface StoredCache { - fun full(): Cache - - fun writeOnly(): Cache - - companion object { - fun from(context: Context): StoredCache = StoredCacheImpl(CacheDatabase.from(context)) + abstract class Factory { + internal abstract fun open(): Cache } } @@ -48,48 +39,3 @@ internal sealed interface CacheResult { data class Miss(val file: DeviceFile, val addedMs: Long?) : CacheResult } - -private class StoredCacheImpl(private val database: CacheDatabase) : StoredCache { - override fun full() = CacheImpl(database.cachedSongsDao()) - - override fun writeOnly() = WriteOnlyCacheImpl(database.cachedSongsDao()) -} - -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.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.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 96f50b03b..a9f12af2a 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -44,7 +44,11 @@ import org.oxycblt.musikr.util.splitEscaped @Database(entities = [CachedSong::class], version = 50, exportSchema = false) internal abstract class CacheDatabase : RoomDatabase() { - abstract fun cachedSongsDao(): CacheInfoDao + abstract fun visibleDao(): VisibleCacheDao + + abstract fun invisibleDao(): InvisibleCacheDao + + abstract fun writeDao(): CacheWriteDao companion object { fun from(context: Context) = @@ -56,7 +60,7 @@ internal abstract class CacheDatabase : RoomDatabase() { } @Dao -internal interface CacheInfoDao { +internal interface VisibleCacheDao { @Query("SELECT * FROM CachedSong WHERE uri = :uri") suspend fun selectSong(uri: String): CachedSong? @@ -68,7 +72,16 @@ internal interface CacheInfoDao { @Query("UPDATE cachedsong SET touchedNs = :nowNs WHERE uri = :uri") suspend fun updateTouchedNs(uri: String, nowNs: Long) +} +@Dao +internal interface InvisibleCacheDao { + @Query("SELECT addedMs FROM CachedSong WHERE uri = :uri") + suspend fun selectAddedMs(uri: String): Long? +} + +@Dao +internal interface CacheWriteDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong) @Query("DELETE FROM CachedSong WHERE touchedNs < :now") diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt new file mode 100644 index 000000000..fca633e5d --- /dev/null +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt @@ -0,0 +1,70 @@ +package org.oxycblt.musikr.cache + +import android.content.Context +import org.oxycblt.musikr.cover.StoredCovers +import org.oxycblt.musikr.fs.DeviceFile +import org.oxycblt.musikr.pipeline.RawSong + +interface StoredCache { + fun visible(): Cache.Factory + + fun invisible(): Cache.Factory + + companion object { + fun from(context: Context): StoredCache = StoredCacheImpl(CacheDatabase.from(context)) + } +} + +private class StoredCacheImpl(private val cacheDatabase: CacheDatabase) : StoredCache { + override fun visible(): Cache.Factory = VisibleStoredCache.Factory(cacheDatabase) + + override fun invisible(): Cache.Factory = InvisibleStoredCache.Factory(cacheDatabase) +} + +private abstract class BaseStoredCache(protected val writeDao: CacheWriteDao) : Cache() { + private val created = System.nanoTime() + + override suspend fun write(song: RawSong) = + writeDao.updateSong(CachedSong.fromRawSong(song)) + + override suspend fun finalize() { + // Anything not create during this cache's use implies that it has not been + // access during this run and should be pruned. + writeDao.pruneOlderThan(created) + } +} + +private class VisibleStoredCache( + private val visibleDao: VisibleCacheDao, + writeDao: CacheWriteDao +) : BaseStoredCache(writeDao) { + override suspend fun read(file: DeviceFile, storedCovers: StoredCovers): CacheResult { + val song = + visibleDao.selectSong(file.uri.toString()) ?: return CacheResult.Miss(file, null) + 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. + visibleDao.touch(file.uri.toString()) + return CacheResult.Hit(song.intoRawSong(file, storedCovers)) + } + + class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() { + override fun open() = VisibleStoredCache(cacheDatabase.visibleDao(), cacheDatabase.writeDao()) + } +} + +private class InvisibleStoredCache( + private val invisibleCacheDao: InvisibleCacheDao, + writeDao: CacheWriteDao +) : BaseStoredCache(writeDao) { + override suspend fun read(file: DeviceFile, storedCovers: StoredCovers) = + CacheResult.Miss(file, invisibleCacheDao.selectAddedMs(file.uri.toString())) + + class Factory(private val cacheDatabase: CacheDatabase) : Cache.Factory() { + override fun open() = InvisibleStoredCache(cacheDatabase.invisibleDao(), cacheDatabase.writeDao()) + } +} 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 2e6890128..7ef83627c 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.musikr.pipeline import android.content.Context @@ -52,7 +52,8 @@ internal interface ExtractStep { MetadataExtractor.new(), TagParser.new(), storage.cache, - storage.storedCovers) + storage.storedCovers + ) } } @@ -60,11 +61,11 @@ private class ExtractStepImpl( private val context: Context, private val metadataExtractor: MetadataExtractor, private val tagParser: TagParser, - private val cache: Cache, + private val cacheFactory: Cache.Factory, private val storedCovers: MutableStoredCovers ) : ExtractStep { override fun extract(nodes: Flow): Flow { - val cacheTimestamp = cache.lap() + val cache = cacheFactory.open() val addingMs = System.currentTimeMillis() val filterFlow = nodes.divert { @@ -113,13 +114,13 @@ private class ExtractStepImpl( val metadata = fds.mapNotNull { fileWith -> - wrap(fileWith.file) { _ -> - metadataExtractor - .extract(fileWith.with) - ?.let { FileWith(fileWith.file, it) } - .also { withContext(Dispatchers.IO) { fileWith.with.close() } } - } + wrap(fileWith.file) { _ -> + metadataExtractor + .extract(fileWith.with) + ?.let { FileWith(fileWith.file, it) } + .also { withContext(Dispatchers.IO) { fileWith.with.close() } } } + } .flowOn(Dispatchers.IO) // Covers are pretty big, so cap the amount of parsed metadata in-memory to at most // 8 to minimize GCs. @@ -148,18 +149,20 @@ private class ExtractStepImpl( .buffer(Channel.UNLIMITED) } .flattenMerge() - .onCompletion { - cache.prune(cacheTimestamp) - } - return merge( + val merged = merge( filterFlow.manager, readDistributedFlow.manager, cacheFlow.manager, cachedSongs, writeDistributedFlow.manager, writtenSongs, - playlistNodes) + playlistNodes + ) + + return merged.onCompletion { + cache.finalize() + } } private data class FileWith(val file: DeviceFile, val with: T)