feat(android): port audio_cache_index (M8 4.2 slice 5)
Mirrors flutter_client/lib/cache/db.dart's AudioCacheIndex — one row
per fully-downloaded audio file. Drives the 2-bucket LRU eviction
policy that Phase 12's AudioCacheEvictionWorker will execute.
DAO surface tailored to the eviction worker:
- totalBytes / bytesBySource — sum-of-sizeBytes for cap checks
- evictionCandidates(source) — oldest lastPlayedAt within a source
bucket, NULL lastPlayedAt sorted first (never-played candidates
evict before played ones)
- touchLastPlayed(trackId, instant) — single-column update from
the player on every "ready+playing" transition
- bulk delete by track-id list for batch evictions
CacheSource enum (MANUAL / INCIDENTAL / AUTO_PREFETCH) ported in
Task 4.1's TypeConverters.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package com.fabledsword.minstrel.cache.db
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.fabledsword.minstrel.cache.db.dao.AudioCacheIndexDao
|
||||
import com.fabledsword.minstrel.cache.db.dao.CachedAlbumDao
|
||||
import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
|
||||
import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao
|
||||
@@ -11,6 +12,7 @@ import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao
|
||||
import com.fabledsword.minstrel.cache.db.dao.CachedQuarantineDao
|
||||
import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao
|
||||
import com.fabledsword.minstrel.cache.db.dao.SyncMetadataDao
|
||||
import com.fabledsword.minstrel.cache.db.entities.AudioCacheIndexEntity
|
||||
import com.fabledsword.minstrel.cache.db.entities.CachedAlbumEntity
|
||||
import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity
|
||||
import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity
|
||||
@@ -43,6 +45,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
|
||||
CachedPlaylistEntity::class,
|
||||
CachedPlaylistTrackEntity::class,
|
||||
CachedQuarantineEntity::class,
|
||||
AudioCacheIndexEntity::class,
|
||||
],
|
||||
version = 1,
|
||||
exportSchema = true,
|
||||
@@ -57,4 +60,5 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun cachedPlaylistDao(): CachedPlaylistDao
|
||||
abstract fun cachedPlaylistTrackDao(): CachedPlaylistTrackDao
|
||||
abstract fun cachedQuarantineDao(): CachedQuarantineDao
|
||||
abstract fun audioCacheIndexDao(): AudioCacheIndexDao
|
||||
}
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.fabledsword.minstrel.cache.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.fabledsword.minstrel.cache.db.CacheSource
|
||||
import com.fabledsword.minstrel.cache.db.entities.AudioCacheIndexEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface AudioCacheIndexDao {
|
||||
/** Used to render the "Downloaded" / "Offline" listing. */
|
||||
@Query("SELECT * FROM audio_cache_index ORDER BY lastPlayedAt DESC NULLS LAST")
|
||||
fun observeAll(): Flow<List<AudioCacheIndexEntity>>
|
||||
|
||||
@Query("SELECT * FROM audio_cache_index WHERE trackId = :trackId")
|
||||
suspend fun get(trackId: String): AudioCacheIndexEntity?
|
||||
|
||||
@Query("SELECT * FROM audio_cache_index WHERE trackId IN (:trackIds)")
|
||||
suspend fun getByTrackIds(trackIds: List<String>): List<AudioCacheIndexEntity>
|
||||
|
||||
/** All cached track IDs — bucket-membership lookup for eviction. */
|
||||
@Query("SELECT trackId FROM audio_cache_index")
|
||||
suspend fun allCachedTrackIds(): List<String>
|
||||
|
||||
/** Eviction-candidate ordering — oldest lastPlayedAt first within a source. */
|
||||
@Query(
|
||||
"SELECT * FROM audio_cache_index " +
|
||||
"WHERE source = :source " +
|
||||
"ORDER BY lastPlayedAt ASC NULLS FIRST, cachedAt ASC",
|
||||
)
|
||||
suspend fun evictionCandidates(source: CacheSource): List<AudioCacheIndexEntity>
|
||||
|
||||
/** Sum of bytes across rows; the rolling/liked bucket caps compare against this. */
|
||||
@Query("SELECT COALESCE(SUM(sizeBytes), 0) FROM audio_cache_index")
|
||||
suspend fun totalBytes(): Long
|
||||
|
||||
@Query(
|
||||
"SELECT COALESCE(SUM(sizeBytes), 0) FROM audio_cache_index WHERE source = :source",
|
||||
)
|
||||
suspend fun bytesBySource(source: CacheSource): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsert(row: AudioCacheIndexEntity)
|
||||
|
||||
@Query(
|
||||
"UPDATE audio_cache_index SET lastPlayedAt = :at WHERE trackId = :trackId",
|
||||
)
|
||||
suspend fun touchLastPlayed(trackId: String, at: kotlinx.datetime.Instant)
|
||||
|
||||
@Query("DELETE FROM audio_cache_index WHERE trackId = :trackId")
|
||||
suspend fun deleteByTrackId(trackId: String)
|
||||
|
||||
@Query("DELETE FROM audio_cache_index WHERE trackId IN (:trackIds)")
|
||||
suspend fun deleteByTrackIds(trackIds: List<String>)
|
||||
|
||||
@Query("DELETE FROM audio_cache_index")
|
||||
suspend fun clear()
|
||||
}
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
package com.fabledsword.minstrel.cache.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.fabledsword.minstrel.cache.db.CacheSource
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
/**
|
||||
* One row per fully-downloaded audio file. Mirrors
|
||||
* `flutter_client/lib/cache/db.dart`'s `AudioCacheIndex` Drift table.
|
||||
*
|
||||
* Drives the 2-bucket LRU eviction (Phase 12 AudioCacheEvictionWorker):
|
||||
* - `incidental` files (streamed-and-cached side effect) evict first
|
||||
* - `autoPrefetch` second
|
||||
* - `manual` (user explicitly pinned) evict last
|
||||
*
|
||||
* `cachedAt` = when we downloaded. `lastPlayedAt` = when the user last
|
||||
* played it; drives the offline "recently played" ordering and is the
|
||||
* tiebreaker for LRU eviction within a source bucket. Nullable on
|
||||
* brand-new rows until the first post-cache play touches them.
|
||||
*/
|
||||
@Entity(tableName = "audio_cache_index")
|
||||
data class AudioCacheIndexEntity(
|
||||
@PrimaryKey val trackId: String,
|
||||
val path: String,
|
||||
val sizeBytes: Int,
|
||||
val cachedAt: Instant = Clock.System.now(),
|
||||
val lastPlayedAt: Instant? = null,
|
||||
val source: CacheSource,
|
||||
)
|
||||
Reference in New Issue
Block a user