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)