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