restoreSession rebuilds the waveform and the cache badge from the backend, and a direct album arrival gets its artist portrait

Two bugs Kyle reported, both in what a session restore does NOT bring back.

THE LANE AND THE STREAM BADGE. Kyle: after the machine sleeps, "the playbar
waveform goes away and the track says streming even though its cashed". The
trigger is not sleep — it is the frontend restarting with a track loaded, which
WebView2 does on its own under memory pressure (and which a page reload does
too). Neither survives it: the waveform is measured out of the audio bytes and
was never re-requested, so the lane rested as dots while ALSO flagged complete;
and streamReceived/streamTotal reset to 0, which is what the Now Playing header
picks between "transferring", "Cached · size" and "Streaming from <peer>" with —
both at zero fell through to the last, for a file that finished downloading
sessions ago. His read was exactly right: it WAS cached and the label was lying.

Both are now rebuilt from the BACKEND's truth rather than the snapshot. The
restore fires the same final-quality waveform request the stream-completion path
uses (it answers from whatever the cache holds — full, partial or nothing, each
rendering honestly) and asks stream_cache_len for the real cached byte count,
setting the counters only when the cache is COMPLETE so "Cached" is never
claimed for a prefix. Local tracks get their waveform re-requested too; they had
the same dotted-lane bug. Note the guard compares path+peer, not object
identity: player.queue is a $state proxy, so the item read back is never === the
snapshot object the closure holds.

THE BREADCRUMB'S ARTIST PORTRAIT. Reaching an album directly (search -> album,
skipping the artist page) left a bare glyph where the artist's picture belongs:
trail() synthesises that crumb from the record's own artist NAME, and
metadata::CatalogAlbum carries only the name — no id, no picture. openCatalogAlbum
now resolves the artist seed after the page lands and re-records the crumb in
place (same key), so trail() reads artistArt/artistId out of it. The resolver
shares the album header's own cache and in-flight dedupe, so this is the lookup
the page mounting already pays for. It also heals the crumb's CLICK, which had
artistId null on every direct arrival.

Verified live: reloading the app in its broken state restored a 2000-peak
waveform and flipped the header to "Cached · 14.6 MB"; Daft Punk's portrait
shows on a direct-from-search album arrival. 98/98 tests.
This commit is contained in:
KeelrLeek committed 2026-08-31 05:53:18 -05:00
1 parent 73a677d35c
commit 15746893a9
2 files changed
+56 -1

No files matched your search

+5 -1
View File
@@ -243,7 +243,11 @@ export function trail() {
view: "catalogArtist",
label: here.artist,
artistId: here.artistId ?? null,
art: null,
// A direct arrival's entry learns these a beat after the
// page lands (openCatalogAlbum resolves the artist seed and
// updates the crumb in place) — until then the synthesised
// crumb honestly has no portrait.
art: here.artistArt ?? null,
round: true,
key: `artist:${here.artistId ?? here.artist}`,
},
+51
View File
@@ -3029,6 +3029,27 @@ export async function openCatalogAlbum(albumId, opts = {}) {
art: page.album.cover ?? null,
artist: page.album.artist ?? null,
});
// The artist crumb the trail synthesises for a DIRECT arrival needs
// the portrait and id, and the album payload carries only the name
// (metadata::CatalogAlbum) — so a reader who skipped the artist page
// got a bare disc glyph in the breadcrumb (Kyle, 2026-08-30). The
// seed resolver shares the album header's own cache and in-flight
// dedupe, so this is the same lookup the page mounting already pays
// for; when it lands, the crumb is updated in place (same key) and
// trail() reads artistArt/artistId out of it. Also heals the crumb's
// CLICK, which had artistId null on every direct arrival.
void resolveArtistSeedByName(page.album.artist).then((seed) => {
if (!seed || !active()) return;
recordVisit({
view: "catalogAlbum",
label: page.album.title,
albumId: page.album.id,
art: page.album.cover ?? null,
artist: page.album.artist ?? null,
artistId: seed.id ?? null,
artistArt: seed.picture ?? null,
});
});
// No catalog trackId (e.g. local playback) — match the song by title.
if (catalog.pendingTrackId == null && opts.trackTitle) {
const t = page.tracks.find(
@@ -7395,6 +7416,36 @@ export async function restoreSession() {
// they would had it just been started.
if (item.stream) setAccentFromArtUrl(item.artUrl ?? null);
else setAccentFromArt(item.art ?? null);
// The lane and the stream badge are rebuilt from the BACKEND's
// truth, not the snapshot. Neither survives a reload on its own:
// the waveform was measured out of the audio bytes, and the byte
// counters reset to 0 — which left the lane resting as dots and
// the header claiming "Streaming from <peer>" for a track whose
// cache file finished downloading sessions ago (Kyle, 2026-08-30,
// after an overnight sleep). A final-quality waveform request
// answers from whatever the cache holds — full, partial, or
// nothing, each rendering honestly — and the counters are set
// only when the cache is COMPLETE, so "Cached" is never claimed
// for a prefix.
if (item.stream) {
void api
.streamCacheLen(item.stream.username, item.path)
.then((len) => {
// By value, not identity: player.queue is a $state
// proxy, so reading the item back never === the raw
// snapshot object this closure holds.
const cur = player.queue[player.queueIndex];
if (cur?.path !== item.path || cur?.stream?.username !== item.stream.username) return;
if ((len ?? 0) >= item.stream.size) {
player.streamTotal = item.stream.size;
player.streamReceived = len;
}
})
.catch(() => {});
void loadStreamWaveform(item, true);
} else {
void loadWaveform(item.path, item.segment ?? null);
}
}
// Where you were. The trail and the history arrows are restored first so