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 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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
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
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
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<ExploreNode>): Flow<ExtractedMusic> {
|
||||
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<T>(val file: DeviceFile, val with: T)
|
||||
|
|
Loading…
Reference in a new issue