From 15746893a9e9f7ddcc991e71e5e47a79807462ad Mon Sep 17 00:00:00 2001 From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com> Date: Mon, 31 Aug 2026 05:53:18 -0500 Subject: [PATCH] restoreSession rebuilds the waveform and the cache badge from the backend, and a direct album arrival gets its artist portrait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 " 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. --- src/lib/navigation.svelte.js | 6 ++++- src/lib/state.svelte.js | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/lib/navigation.svelte.js b/src/lib/navigation.svelte.js index bfc3596..d252ec8 100644 --- a/src/lib/navigation.svelte.js +++ b/src/lib/navigation.svelte.js @@ -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}`, }, diff --git a/src/lib/state.svelte.js b/src/lib/state.svelte.js index e754bd9..60a1917 100644 --- a/src/lib/state.svelte.js +++ b/src/lib/state.svelte.js @@ -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 " 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