8df41c3bed
android / Build + lint + test (push) Failing after 1m31s
CacheSettings.prefetchWindow has shipped at default=5 since M8 but the prefetcher itself was deferred to a follow-up. Without it, forward skips re-fetch from the network every time and gapless transitions stall. New AudioPrefetcher singleton subscribes to PlayerController.uiState + AuthStore.cacheSettings, walks queue[currentIndex+1 .. +window], and runs each missing track through Media3's CacheWriter against the same SimpleCache the player reads from (PlayerFactory.simpleCache). DataSpec uses setKey(trackId) to match PlayerController.toMediaItem's setCustomCacheKey(id) — without this the player would miss the cached bytes on read-through. Reconcile is idempotent: CacheWriter is a no-op when bytes are already resident, so distinctUntilChanged on (queue, index, window) gates re-runs and the per-item check is cheap. Window slides cancel in-flight jobs for tracks that have dropped out (skip-prev, queue rebuild) so a stale prefetch doesn't keep the network busy. Eager-constructed via the construct-the-singleton trick in MinstrelApplication, alongside CacheIndexer and CoverPrefetcher. Mirrors flutter_client/lib/cache/prefetcher.dart's user-visible behavior (queue-walk + per-track pin + idempotent reconcile) implemented with the native Media3 primitives (CacheWriter + SimpleCache) instead of Flutter's AudioCacheManager.pin.