diff --git a/src/lib/state.svelte.js b/src/lib/state.svelte.js index 1ddcb9e..9fea2dd 100644 --- a/src/lib/state.svelte.js +++ b/src/lib/state.svelte.js @@ -4447,6 +4447,44 @@ export async function streamCatalogAlbum(album) { // library unavailable — fall through to streaming } } + // Every track of this record remembered and still byte-complete in the + // cache? Queue the whole album from memory — no search, no popup, and + // playable offline. A single unremembered or evicted track falls through + // to the resolver exactly as before, which then re-teaches the memory via + // the queue it builds. + if (album.id) { + const known = await api.catalogAlbum(album.id).then((p) => p?.tracks ?? null, () => null); + if (known?.length) { + const sources = []; + for (const t of known) { + const src = await cachedStreamSourceFor(t.id ?? null, album.artist, t.title); + if (!src) { + sources.length = 0; + break; + } + sources.push({ t, src }); + } + if (sources.length) { + autoAlbum = null; + player.queue = sources.map(({ t, src }) => ({ + path: src.path, + title: t.title, + sub: `${album.artist} — ${album.title}`, + artist: album.artist, + album: album.title ?? null, + duration: src.duration ?? t.duration_secs ?? null, + art: null, + artUrl: album.cover ?? null, + trackId: t.id ?? null, + stream: { username: src.username, size: src.size }, + quality: src.quality ?? null, + })); + player.catalogRef = { albumId: album.id ?? null, trackId: null }; + playQueueIndex(0); + return; + } + } + } if (!requireOnline()) return; autoAlbum = null; player.track = { title: album.title, sub: "finding best source…", source: "stream" }; @@ -4537,6 +4575,11 @@ export async function streamCatalogAlbum(album) { quality: qualityOf(f), }; }); + // One album resolve teaches the memory the WHOLE record, so the next + // press of Play needs no search at all once these files finish + // caching. A folder that never delivers is harmless — its cache stays + // incomplete and the probe rejects it. + for (const it of player.queue) rememberStreamSource(it); player.catalogRef = { albumId: album.id ?? null, trackId: null }; playQueueIndex(0); if (await waitForStreamStart(15000)) return; @@ -5522,6 +5565,30 @@ async function maybeExtendAlbumQueue() { for (let i = idx + 1; i < ctx.tracks.length; i++) { const t = ctx.tracks[i]; if (app.connection !== "online") return; + // A remembered, still-byte-complete source skips this track's + // background search entirely — the common case for a record that + // has streamed before. Same bail guards as the searched path. + const known = await cachedStreamSourceFor(t.id ?? null, t.artist, t.title); + if (known) { + if (autoAlbum !== ctx || !player.queue.includes(anchor)) return; + const item = { + path: known.path, + title: t.title, + sub: t.artist, + artist: t.artist ?? null, + album: ctx.albumTitle ?? null, + duration: known.duration ?? t.duration_secs ?? null, + art: null, + artUrl: ctx.cover, + trackId: t.id, + stream: { username: known.username, size: known.size }, + quality: known.quality ?? null, + }; + player.queue.push(item); + preloadNextStream(); + tryAppendNext(); + continue; + } // Same sanitized ladder as resolve(), inlined because this is a // background append that must not toggle resolver.active (that // would block user-triggered resolutions while it searches). @@ -5559,7 +5626,7 @@ async function maybeExtendAlbumQueue() { responses, t.title, true, ctx.albumTitle, t.duration_secs, t.artist, !artistIncluded, )[0]; if (!best) continue; // no streamable source — skip this track - player.queue.push({ + const item = { path: best.file.path, title: t.title, sub: t.artist, @@ -5571,7 +5638,11 @@ async function maybeExtendAlbumQueue() { trackId: t.id, stream: { username: best.username, size: best.file.size }, quality: qualityOf(best.file), - }); + }; + player.queue.push(item); + // Teach the memory as the fill goes, so the album's LATER tracks + // are known even if playback never reaches them this session. + rememberStreamSource(item); // Both are guarded against repeat work (lastPreloadedPath, // appendedNextPath), so calling them per append costs nothing once // the immediate next track is already prepared.