feat(web/player): self-heal a stale system-playlist / radio queue on total failure
test-web / test (push) Successful in 39s

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>
This commit is contained in:
2026-06-20 12:51:47 -04:00
parent 335d782215
commit 27766ae063
5 changed files with 169 additions and 11 deletions
+20 -5
View File
@@ -18,6 +18,7 @@
import { user } from '$lib/auth/store.svelte';
import { errCode, errMessage } from '$lib/api/errors';
import { playQueue } from '$lib/player/store.svelte';
import { systemPlaylistRefetch } from '$lib/playlists/systemRefetch';
import { pushToast } from '$lib/stores/toast.svelte';
import type { TrackRef } from '$lib/api/types';
@@ -61,16 +62,30 @@
function onPlay(position: number) {
if (!playlistQuery?.data) return;
const data = playlistQuery.data;
// Skip rows where track_id is null (track was removed from library).
const live = playlistQuery.data.tracks
.filter((t) => t.track_id !== null)
.map(toTrackRef);
const live = data.tracks.filter((t) => t.track_id !== null).map(toTrackRef);
// Find which `live` index corresponds to the clicked position. Some
// positions in the original list may be unavailable (filtered out),
// so the index in the playable list != the playlist position.
const clickedTrackID = playlistQuery.data.tracks.find((t) => t.position === position)?.track_id;
const clickedTrackID = data.tracks.find((t) => t.position === position)?.track_id;
const startIdx = live.findIndex((t) => t.id === clickedTrackID);
playQueue(live, Math.max(0, startIdx));
// #968: a system playlist played from its detail page gets the same
// self-heal closure as the home tile, so a stale snapshot re-pulls on
// total failure. (Source attribution is intentionally left to the home
// tile / shuffle path — this only adds the recovery closure.)
const variant = data.system_variant;
if (variant != null) {
playQueue(live, Math.max(0, startIdx), {
refetch: systemPlaylistRefetch({
variant,
playlistId: id,
perArtist: data.seed_artist_id != null
})
});
} else {
playQueue(live, Math.max(0, startIdx));
}
}
function toTrackRef(t: {