Image rendering + player resilience (#968, #980) #103

Merged
bvandeusen merged 11 commits from dev into main 2026-06-20 20:57:39 -04:00
Owner

Cover-art rendering and playback/error resilience across web + Android, plus the server event that drives proactive refresh.

Track A — Image resilience (#968)

One shared cover renderer per client so tiles never sit blank while loading or 404.

  • Web: new Cover.svelte (placeholder while loading + onerror fallback / Disc3 for artists); AlbumCard, ArtistCard, CompactTrackCard reuse it — ArtistCard had no error handler before.
  • Android: ServerImage tracks Coil load state so the per-caller fallback doubles as placeholder + error painter, not just a null-URL guard.

Track B — Player resilience (#968)

  • Auto-skip on load failure (web + Android): a bad track advances instead of dead-ending; "Try again"/stop only once the whole queue is unplayable. Android's "Skipped N tracks" snackbar is now truthful.
  • Self-heal on total failure (web + Android): a fully-unplayable queue re-pulls its source (system playlist via systemShuffle, radio re-seed) and resumes; bounded to one re-pull per exhaustion.
  • Proactive reactivity: server emits playlist.system_rebuilt on the daily 03:00 rebuild + manual refresh; web subscribes over SSE (its first inbound listener), Android via EventsStream — both refresh home/playlist browse views. Fixes the "tab open since yesterday shows yesterday's playlist" report.

Follow-on (#980)

Stale-view "This mix was refreshed · Refresh" banner/snackbar on an open system-playlist detail screen; Refresh re-resolves the rotated variant to the new playlist id (web + Android).

All commits landed on dev with CI green per push.

🤖 Generated with Claude Code

Cover-art rendering and playback/error resilience across web + Android, plus the server event that drives proactive refresh. ## Track A — Image resilience (#968) One shared cover renderer per client so tiles never sit blank while loading or 404. - **Web:** new `Cover.svelte` (placeholder while loading + onerror fallback / Disc3 for artists); `AlbumCard`, `ArtistCard`, `CompactTrackCard` reuse it — `ArtistCard` had no error handler before. - **Android:** `ServerImage` tracks Coil load state so the per-caller fallback doubles as placeholder + error painter, not just a null-URL guard. ## Track B — Player resilience (#968) - **Auto-skip on load failure** (web + Android): a bad track advances instead of dead-ending; "Try again"/stop only once the whole queue is unplayable. Android's "Skipped N tracks" snackbar is now truthful. - **Self-heal on total failure** (web + Android): a fully-unplayable queue re-pulls its source (system playlist via `systemShuffle`, radio re-seed) and resumes; bounded to one re-pull per exhaustion. - **Proactive reactivity:** server emits `playlist.system_rebuilt` on the daily 03:00 rebuild + manual refresh; web subscribes over SSE (its first inbound listener), Android via `EventsStream` — both refresh home/playlist browse views. Fixes the "tab open since yesterday shows yesterday's playlist" report. ## Follow-on (#980) Stale-view "This mix was refreshed · Refresh" banner/snackbar on an open system-playlist detail screen; Refresh re-resolves the rotated variant to the new playlist id (web + Android). All commits landed on `dev` with CI green per push. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bvandeusen added 11 commits 2026-06-20 20:57:32 -04:00
fix(clients): never leave cover tiles blank — shared web <Cover> + Android Coil placeholder/error
test-web / test (push) Successful in 48s
android / Build + lint + test (push) Successful in 4m10s
096a3c0b15
Cover tiles (worst in the "You might like" home row, which surfaces
unplayed items whose art is often not yet backfilled) sat empty while
loading and stayed blank on a 404. The server returns a fast 404; the
gap was missing client-side loading/fallback states.

Web: new shared Cover.svelte owns the loading placeholder + onerror
fallback (static cover, or Disc3 for artists). AlbumCard, ArtistCard and
CompactTrackCard now reuse it instead of three hand-rolled <img> tags
that disagreed on fallback handling — notably ArtistCard had no onerror.

Android: ServerImage tracks Coil's load state so the per-caller fallback
doubles as a placeholder (loading) and an error state (404 / unreachable),
instead of only guarding the null-URL case. All five call sites pass an
explicit size modifier, so the new Box wrapper is layout-safe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A track that fails to load (e.g. a stale system-playlist snapshot pointing
at a rebuilt/removed file) hard-set the player to the 'error' state and
stranded the user on a "Try again" button that just re-queued the same
failing track. Now a load error advances to the next track; the error
state only surfaces once the whole queue has proven unplayable — every
track failed, or we reached the end. A failure streak capped at queue
length stops a fully-broken queue from cycling, and resets on the next
successful play.

Next (Track B cont.): self-heal a stale system-playlist / radio queue by
re-pulling the fresh snapshot on total failure, plus the Android
equivalent. Issue #968.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
onPlayerError fired a `load_failed` event and the snackbar reporter coalesced
it into "Skipped N unplayable tracks" — but nothing actually skipped, so a
bad/stale track stranded playback while the toast claimed otherwise. Mirror
the zero_duration path: advance to the next item and re-prepare (a load error
leaves the player IDLE), or stop at the end. Forward-only bounds a fully-
unplayable queue. Web parity with 2a8de82a. Issue #968.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the whole queue proves unplayable (e.g. a tab left open across the
daily system-playlist rebuild — the exact stale-snapshot case), the player
now re-pulls the fresh snapshot and resumes instead of dead-ending on
"Try again". The seeder hands the store an opaque refetch closure so the
store stays decoupled from the playlist API and the per-artist
(songs_like_artist) identity problem: single-instance variants re-pull via
systemShuffle, per-artist mixes via getPlaylist(id), radio re-seeds from
its track. Bounded to one self-heal per exhaustion (reset on the next
successful play) so a still-broken refresh can't loop; "Try again" stays
the genuine last resort. Wired from PlaylistCard, the playlist detail page,
and playRadio. Issue #968.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Web parity with 27766ae0. When a load error exhausts a fully-unplayable
queue, re-pull the source instead of stopping: a bare-variant source is a
refreshable system playlist (re-pull via PlaylistsRepository.systemShuffle),
"radio:<seed>" re-seeds via RadioController. Reads the source from the
current MediaItem extra; bounded to one re-pull per exhaustion (reset when
a track next loads with real audio) so a still-stale refresh can't loop.
Album / artist / user-playlist / offline sources have nothing to refresh
and still stop. Issue #968.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(server): emit playlist.system_rebuilt on daily + manual system-playlist rebuild
test-go / test (push) Successful in 28s
test-go / integration (push) Successful in 4m27s
16f76ea707
The daily 03:00 scheduler rebuild (and the manual refresh endpoint) replace
a user's system playlists + You-might-like rows but published no event, so a
client left open across the rebuild served yesterday's snapshot until a
manual reload — the stale-tab case behind #968. Add a user-scoped
playlist.system_rebuilt event (envelope {kind,user_id,data:{}}) from both the
scheduler (bus threaded into NewScheduler) and handleSystemPlaylistRefresh.
Clients consume it to invalidate home / system-playlist views and proactively
re-pull a stale active queue. Issue #968.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The web client only ever SENT events; it had no inbound SSE listener, so a
tab left open across the daily system-playlist rebuild kept showing
yesterday's home + playlist snapshots until a manual reload (the stale-
browse-view bug behind #968). Add useServerEvents(): opens /api/events/stream
while authenticated and, on playlist.system_rebuilt, invalidates the home,
playlists, and system-playlist-status query caches. Deliberately does not
disturb the active playback queue — that self-heals on the failure path.
Issue #968.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(android/home): refresh Home on playlist.system_rebuilt
android / Build + lint + test (push) Successful in 3m46s
a23e2e36ca
Parity with the web SSE consumer (5a80a1e4). HomeViewModel now subscribes to
EventsStream and re-pulls Home (refreshIndex + system-playlist status) when
the server emits playlist.system_rebuilt — the daily 03:00 rebuild or a
manual refresh — so the system-playlist tiles and You-might-like rows reflect
the new snapshot without a manual reload. Browse-only: the active playback
queue is left to self-heal on the failure path. Issue #968.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Playing a system playlist from /playlists/<id> previously sent no source, so
it never advanced that playlist's rotation — inconsistent with the home tile
(and the Android detail screen, which already tags the variant). Pass
source: variant alongside the existing self-heal closure so a play is
attributed regardless of the surface it started from. Issue #968.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#980. When the daily rebuild fires while a system-playlist detail page is
open, its cached data goes stale and can't be refetched in place — the
playlist id rotated, so the old id 404s. serverEvents now exposes a monotonic
rebuild counter; the detail page shows a "this mix was refreshed" banner with
a Refresh that re-resolves the variant (systemShuffle) to the new playlist id
and navigates there. No forced redirect, no auto-reload — the user refreshes
on their terms. Functional behaviors were already correct (tapping a song
plays it; tiles load the current mix); this closes the cosmetic list-staleness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#980, parity with web e932ab43. When playlist.system_rebuilt arrives (SSE)
while a system-playlist detail screen is open, the ViewModel marks it stale and
the screen shows an indefinite "This mix was refreshed · Refresh" snackbar.
Refresh re-resolves the rotated variant via PlaylistsRepository.systemShuffle
and reuses the existing regenerated navigate-replace flow to land on the fresh
playlist id — without triggering another server rebuild (unlike the manual
regenerate button). Dismiss clears the flag. Functional behaviors were already
correct; this closes the cosmetic stale-list gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bvandeusen merged commit 0de2437689 into main 2026-06-20 20:57:39 -04:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#103