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.