From fbd6264037352ca8323450b74ed55cd2e41f8ee2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 13:17:12 -0400 Subject: [PATCH] feat(android): offline pools filter to cache-resident tracks (#38 slice 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ShuffleSource intersects the play-recency list with the Media3 SimpleCache key set, so the offline Recently-played / Liked pools only offer tracks whose audio is actually on disk. Flutter's index holds fully-cached tracks only; Android's records plays, so this intersection restores parity — a played-then-evicted track no longer surfaces in a pool where it would fail to play offline. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/cache/ShuffleSource.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/ShuffleSource.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/ShuffleSource.kt index f23d1ec9..6cf6db84 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/cache/ShuffleSource.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/ShuffleSource.kt @@ -5,6 +5,7 @@ import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao import com.fabledsword.minstrel.library.data.toDomain import com.fabledsword.minstrel.likes.data.LikesRepository import com.fabledsword.minstrel.models.TrackRef +import com.fabledsword.minstrel.player.PlayerFactory import javax.inject.Inject import javax.inject.Singleton @@ -19,6 +20,12 @@ private const val POOL_LIMIT = 100 * storage/eviction-only and never filters playback — exactly what * this relies on. * + * Divergence note: Flutter's `audioCacheIndex` only holds fully-cached + * tracks, so its pools are inherently resident. Android's index records + * plays (see `CacheIndexer`), so the recency list is intersected with + * true Media3 SimpleCache residency here — the pool never offers a + * played-then-evicted track that would fail to play offline. + * * Surfaced on Home's Playlists row only when offline; tap shuffles + * plays the pool from the cache. */ @@ -27,18 +34,26 @@ class ShuffleSource @Inject constructor( private val cacheIndexDao: AudioCacheIndexDao, private val trackDao: CachedTrackDao, private val likes: LikesRepository, + private val playerFactory: PlayerFactory, ) { /** Cached tracks, most-recently-played first (liked included). */ suspend fun recentlyPlayed(limit: Int = POOL_LIMIT): List = - materialize(cacheIndexDao.trackIdsByRecency()).take(limit) + materialize(residentIdsByRecency()).take(limit) /** Cached tracks that are in the user's liked set, recency-ordered. */ suspend fun liked(limit: Int = POOL_LIMIT): List { val likedSet = likes.likedTrackIds() - val ids = cacheIndexDao.trackIdsByRecency().filter { it in likedSet } + val ids = residentIdsByRecency().filter { it in likedSet } return materialize(ids).take(limit) } + /** Recency-ordered ids whose audio is actually resident in the cache. */ + private suspend fun residentIdsByRecency(): List { + val resident = runCatching { playerFactory.simpleCache.keys.toSet() } + .getOrDefault(emptySet()) + return cacheIndexDao.trackIdsByRecency().filter { it in resident } + } + /** Materialize ordered ids into TrackRefs from cached metadata, preserving order. */ private suspend fun materialize(orderedIds: List): List { if (orderedIds.isEmpty()) return emptyList()