4 Commits

Author SHA1 Message Date
bvandeusen 3f61079c02 feat(offline): #427 S2(+S3) — two-bucket cache model
Replaces the single 5GB capBytes with independent Liked + Rolling
budgets (5GB each default). Bucket = liked-ness, NOT CacheSource:
a cached track currently in the liked set is charged to / evicted
under Liked; everything else is Rolling. Storage-only dedup — it
never filters playback (S4's offline lists query the whole index).

- db.dart: schema 8→9, AudioCacheIndex.lastPlayedAt (real play
  recency for S4 + rolling LRU; migration backfills to cachedAt).
  drift codegen run.
- cache_settings: likedCapBytes + rollingCapBytes (+ setters); old
  cache_cap_bytes key dropped, defaults reapply (not data loss).
- audio_cache_manager: touch(); bucketUsage() counts orphan
  partials (LockCaching files never indexed) as Rolling so the cap
  truly bounds disk; evictBuckets() drains non-liked LRU then
  sweeps orphans, Liked only by its own (large) cap — normal use
  never evicts the user's liked library.
- prefetcher → evictBuckets with the cached liked set.
- storage_section: two cap selectors + per-bucket usage (folds in
  S3 to avoid a broken intermediate).
- Explicit Download dropped: removed album + playlist Download
  buttons, autoPlaylist pins, now-unused imports.
- Tests updated/compiled (drift-cohort tests are CI-skipped).

High blast radius (eviction deletes files) — liked-protective by
design; needs operator device-check before "done".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:07:45 -04:00
bvandeusen fb95a462fb fix(player): align audio + UI on track change, prewarm covers + palette
Four related fixes to the player flow that together remove the
audio↔UI lag on track change:

1. **Prefetcher pre-warms covers + palette for the next N tracks.**
   The existing prefetcher pinned audio files only. When auto-advance
   landed on the next track, the cover bytes were cold → mediaItem
   broadcast with artUri=null → now-playing screen stalled in
   _scheduleSwap awaiting precacheImage of a file that didn't exist
   yet. Each upcoming queue item now also fires
   AlbumCoverCache.getOrFetch (writes bytes to disk so _toMediaItem's
   peekCached returns the path) and AlbumColorCache.getOrExtract
   (memoizes the dominant color). Both fire-and-forget; idempotent
   if already cached.

2. **AlbumColorCache.peekColor sync getter** so the now-playing
   fast-path can read the memoized color without awaiting a Future.

3. **_scheduleSwap fast path** when cover bytes + palette are
   already cached (the common in-queue auto-advance case): commit
   _displayedMedia / _displayedDominant synchronously in setState
   without awaiting. The async preload remains as the slow-path
   fallback for genuine cold cache. This is what closes the gap
   the user reported: "art is loading and metadata hasn't updated
   but the new song is playing."

4. **setQueueFromTracks: build source before broadcasting queue /
   mediaItem.** Previously we broadcast immediately for snappy UI;
   if _buildAudioSource threw, the UI showed the new track while
   the player held the old source. Now: build first, broadcast
   only on success. Source build is sub-100ms on warm cache so the
   tap response cost is imperceptible. _suppressIndexUpdates is set
   around setAudioSources + broadcast so a transient currentIndex
   emission can't cross-broadcast the OLD queue's entry at the NEW
   index.

5. **playbackEventStream error handler skips past failing tracks.**
   Previously errors only logged. The player would go silent on a
   404 / decoder failure but mediaItem stayed on the failed track —
   user saw "now playing X" with no audio. Now seekToNext on error;
   if at queue tail, pause cleanly so PlaybackState reflects idle.
2026-05-14 16:42:19 -04:00
bvandeusen 0d171490c3 fix(flutter): analyze errors after Plan B push
Cleared 5 errors + 1 warning + 1 info from CI flutter analyze on 5114a81:

- prefetcher.dart: Riverpod listener callbacks used (_, _) for two
  placeholder params — Dart 3 enforces unique names. Changed to (_, __).
- prefetcher.dart: Riverpod 3's AsyncValue exposes `.value` (nullable)
  not `.valueOrNull`. Three call sites updated.
- sync_controller.dart: doc comment had `?since=<cursor>` → analyzer
  warned about unintended HTML. Wrapped in backticks with `{cursor}`.
- sync_controller.dart: delete loop over heterogeneous Table list
  inferred as List<Table>; drift's delete() expects TableInfo. Unrolled
  to explicit per-table deletes.
- audio_cache_manager_test.dart: db.into(...).insertAll([...]) doesn't
  exist on InsertStatement — only insert/insertOnConflictUpdate. Used
  db.batch((b) => b.insertAll(table, [...])) instead, in two test cases.
- audio_handler.dart: LockCachingAudioSource is marked experimental in
  just_audio. Added // ignore: experimental_member_use — operator
  acknowledged the experimental status during brainstorming and we're
  the project; CI's --fatal-infos would otherwise gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:18:38 -04:00
bvandeusen 536350380f feat(flutter/cache): Prefetcher — queue-ahead pre-download window
Listens to mediaItemProvider + cacheSettingsProvider. On track change
(or settings change), computes the [currentIdx, currentIdx + N] window
in the queue and pins each uncached track via AudioCacheManager with
source: autoPrefetch.

Window N comes from cacheSettingsProvider.prefetchWindow (default 5,
configurable in Settings → Storage card).

Smoke-test only at the unit level — real coverage via on-device pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:19:01 -04:00