musikr: refactor cache api
To make the pruning system more agnostic and "extendable"
This commit is contained in:
parent
3f364dc5c6
commit
61fd11fe04
6 changed files with 108 additions and 76 deletions
|
@ -362,7 +362,7 @@ constructor(
|
||||||
|
|
||||||
val currentRevision = musicSettings.revision
|
val currentRevision = musicSettings.revision
|
||||||
val newRevision = currentRevision?.takeIf { withCache } ?: UUID.randomUUID()
|
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 covers = MutableRevisionedStoredCovers(context, newRevision)
|
||||||
val storage = Storage(cache, covers, storedPlaylists)
|
val storage = Storage(cache, covers, storedPlaylists)
|
||||||
val interpretation = Interpretation(nameFactory, separators)
|
val interpretation = Interpretation(nameFactory, separators)
|
||||||
|
|
|
@ -25,7 +25,7 @@ import org.oxycblt.musikr.tag.interpret.Naming
|
||||||
import org.oxycblt.musikr.tag.interpret.Separators
|
import org.oxycblt.musikr.tag.interpret.Separators
|
||||||
|
|
||||||
data class Storage(
|
data class Storage(
|
||||||
val cache: Cache,
|
val cache: Cache.Factory,
|
||||||
val storedCovers: MutableStoredCovers,
|
val storedCovers: MutableStoredCovers,
|
||||||
val storedPlaylists: StoredPlaylists
|
val storedPlaylists: StoredPlaylists
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,28 +18,19 @@
|
||||||
|
|
||||||
package org.oxycblt.musikr.cache
|
package org.oxycblt.musikr.cache
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
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.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)
|
internal abstract suspend fun finalize()
|
||||||
}
|
|
||||||
|
|
||||||
interface StoredCache {
|
abstract class Factory {
|
||||||
fun full(): Cache
|
internal abstract fun open(): Cache
|
||||||
|
|
||||||
fun writeOnly(): Cache
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(context: Context): StoredCache = StoredCacheImpl(CacheDatabase.from(context))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,48 +39,3 @@ internal sealed interface CacheResult {
|
||||||
|
|
||||||
data class Miss(val file: DeviceFile, val addedMs: Long?) : 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -44,7 +44,11 @@ import org.oxycblt.musikr.util.splitEscaped
|
||||||
|
|
||||||
@Database(entities = [CachedSong::class], version = 50, exportSchema = false)
|
@Database(entities = [CachedSong::class], version = 50, exportSchema = false)
|
||||||
internal abstract class CacheDatabase : RoomDatabase() {
|
internal abstract class CacheDatabase : RoomDatabase() {
|
||||||
abstract fun cachedSongsDao(): CacheInfoDao
|
abstract fun visibleDao(): VisibleCacheDao
|
||||||
|
|
||||||
|
abstract fun invisibleDao(): InvisibleCacheDao
|
||||||
|
|
||||||
|
abstract fun writeDao(): CacheWriteDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(context: Context) =
|
fun from(context: Context) =
|
||||||
|
@ -56,7 +60,7 @@ internal abstract class CacheDatabase : RoomDatabase() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
internal interface CacheInfoDao {
|
internal interface VisibleCacheDao {
|
||||||
@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?
|
||||||
|
|
||||||
|
@ -68,7 +72,16 @@ internal interface CacheInfoDao {
|
||||||
|
|
||||||
@Query("UPDATE cachedsong SET touchedNs = :nowNs WHERE uri = :uri")
|
@Query("UPDATE cachedsong SET touchedNs = :nowNs WHERE uri = :uri")
|
||||||
suspend fun updateTouchedNs(uri: String, nowNs: Long)
|
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)
|
@Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateSong(cachedSong: CachedSong)
|
||||||
|
|
||||||
@Query("DELETE FROM CachedSong WHERE touchedNs < :now")
|
@Query("DELETE FROM CachedSong WHERE touchedNs < :now")
|
||||||
|
|
70
musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt
vendored
Normal file
70
musikr/src/main/java/org/oxycblt/musikr/cache/StoredCache.kt
vendored
Normal file
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.musikr.pipeline
|
package org.oxycblt.musikr.pipeline
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -52,7 +52,8 @@ internal interface ExtractStep {
|
||||||
MetadataExtractor.new(),
|
MetadataExtractor.new(),
|
||||||
TagParser.new(),
|
TagParser.new(),
|
||||||
storage.cache,
|
storage.cache,
|
||||||
storage.storedCovers)
|
storage.storedCovers
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,11 +61,11 @@ private class ExtractStepImpl(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val metadataExtractor: MetadataExtractor,
|
private val metadataExtractor: MetadataExtractor,
|
||||||
private val tagParser: TagParser,
|
private val tagParser: TagParser,
|
||||||
private val cache: Cache,
|
private val cacheFactory: Cache.Factory,
|
||||||
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 cacheTimestamp = cache.lap()
|
val cache = cacheFactory.open()
|
||||||
val addingMs = System.currentTimeMillis()
|
val addingMs = System.currentTimeMillis()
|
||||||
val filterFlow =
|
val filterFlow =
|
||||||
nodes.divert {
|
nodes.divert {
|
||||||
|
@ -113,13 +114,13 @@ private class ExtractStepImpl(
|
||||||
|
|
||||||
val metadata =
|
val metadata =
|
||||||
fds.mapNotNull { fileWith ->
|
fds.mapNotNull { fileWith ->
|
||||||
wrap(fileWith.file) { _ ->
|
wrap(fileWith.file) { _ ->
|
||||||
metadataExtractor
|
metadataExtractor
|
||||||
.extract(fileWith.with)
|
.extract(fileWith.with)
|
||||||
?.let { FileWith(fileWith.file, it) }
|
?.let { FileWith(fileWith.file, it) }
|
||||||
.also { withContext(Dispatchers.IO) { fileWith.with.close() } }
|
.also { withContext(Dispatchers.IO) { fileWith.with.close() } }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
// Covers are pretty big, so cap the amount of parsed metadata in-memory to at most
|
// Covers are pretty big, so cap the amount of parsed metadata in-memory to at most
|
||||||
// 8 to minimize GCs.
|
// 8 to minimize GCs.
|
||||||
|
@ -148,18 +149,20 @@ private class ExtractStepImpl(
|
||||||
.buffer(Channel.UNLIMITED)
|
.buffer(Channel.UNLIMITED)
|
||||||
}
|
}
|
||||||
.flattenMerge()
|
.flattenMerge()
|
||||||
.onCompletion {
|
|
||||||
cache.prune(cacheTimestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return merge(
|
val merged = merge(
|
||||||
filterFlow.manager,
|
filterFlow.manager,
|
||||||
readDistributedFlow.manager,
|
readDistributedFlow.manager,
|
||||||
cacheFlow.manager,
|
cacheFlow.manager,
|
||||||
cachedSongs,
|
cachedSongs,
|
||||||
writeDistributedFlow.manager,
|
writeDistributedFlow.manager,
|
||||||
writtenSongs,
|
writtenSongs,
|
||||||
playlistNodes)
|
playlistNodes
|
||||||
|
)
|
||||||
|
|
||||||
|
return merged.onCompletion {
|
||||||
|
cache.finalize()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class FileWith<T>(val file: DeviceFile, val with: T)
|
private data class FileWith<T>(val file: DeviceFile, val with: T)
|
||||||
|
|
Loading…
Reference in a new issue