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.library.data.toDomain
|
||||||
import com.fabledsword.minstrel.likes.data.LikesRepository
|
import com.fabledsword.minstrel.likes.data.LikesRepository
|
||||||
import com.fabledsword.minstrel.models.TrackRef
|
import com.fabledsword.minstrel.models.TrackRef
|
||||||
|
import com.fabledsword.minstrel.player.PlayerFactory
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -19,6 +20,12 @@ private const val POOL_LIMIT = 100
|
|||||||
* storage/eviction-only and never filters playback — exactly what
|
* storage/eviction-only and never filters playback — exactly what
|
||||||
* this relies on.
|
* 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 +
|
* Surfaced on Home's Playlists row only when offline; tap shuffles +
|
||||||
* plays the pool from the cache.
|
* plays the pool from the cache.
|
||||||
*/
|
*/
|
||||||
@@ -27,18 +34,26 @@ class ShuffleSource @Inject constructor(
|
|||||||
private val cacheIndexDao: AudioCacheIndexDao,
|
private val cacheIndexDao: AudioCacheIndexDao,
|
||||||
private val trackDao: CachedTrackDao,
|
private val trackDao: CachedTrackDao,
|
||||||
private val likes: LikesRepository,
|
private val likes: LikesRepository,
|
||||||
|
private val playerFactory: PlayerFactory,
|
||||||
) {
|
) {
|
||||||
/** Cached tracks, most-recently-played first (liked included). */
|
/** Cached tracks, most-recently-played first (liked included). */
|
||||||
suspend fun recentlyPlayed(limit: Int = POOL_LIMIT): List<TrackRef> =
|
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. */
|
/** Cached tracks that are in the user's liked set, recency-ordered. */
|
||||||
suspend fun liked(limit: Int = POOL_LIMIT): List<TrackRef> {
|
suspend fun liked(limit: Int = POOL_LIMIT): List<TrackRef> {
|
||||||
val likedSet = likes.likedTrackIds()
|
val likedSet = likes.likedTrackIds()
|
||||||
val ids = cacheIndexDao.trackIdsByRecency().filter { it in likedSet }
|
val ids = residentIdsByRecency().filter { it in likedSet }
|
||||||
return materialize(ids).take(limit)
|
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. */
|
/** Materialize ordered ids into TrackRefs from cached metadata, preserving order. */
|
||||||
private suspend fun materialize(orderedIds: List<String>): List<TrackRef> {
|
private suspend fun materialize(orderedIds: List<String>): List<TrackRef> {
|
||||||
if (orderedIds.isEmpty()) return emptyList()
|
if (orderedIds.isEmpty()) return emptyList()
|
||||||
|
|||||||
Reference in New Issue
Block a user