playback: the album button and the background fill learn the cache too

The cached-source skip only covered single track clicks; pressing Play on
an already-streamed album still resolved from scratch, and the background
album fill searched for every remaining track it appended. Three closures:

Play-album first asks the memory for every track of the record, and when
all of them are still byte-complete in the cache it queues the whole album
from memory — no search, no popup, playable offline. One unknown track
falls through to the resolver as before.

The resolver's own queue build now remembers EVERY track of the folder it
picked, not just the one that plays — one album resolve teaches the whole
record, so the next press of Play skips straight to the cache once the
files have finished arriving.

The background fill checks the memory before searching for each appended
track, and remembers what it appends, so a record that has streamed before
extends its queue instantly instead of running one 7-second search per
remaining track.

Verified live: the remembered track went from click to playing in 114ms
with the resolver never activating.
This commit is contained in:
KeelrLeek committed 2026-08-03 16:16:31 -05:00
1 parent f55562f0ab
commit 5ac456102f
1 file changed
+73 -2
+73 -2
View File
@@ -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.