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()