feat(android): offline pools filter to cache-resident tracks (#38 slice 3)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<TrackRef> =
|
||||
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<TrackRef> {
|
||||
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<String> {
|
||||
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<String>): List<TrackRef> {
|
||||
if (orderedIds.isEmpty()) return emptyList()
|
||||
|
||||
Reference in New Issue
Block a user