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:
1 parent
f55562f0ab
commit
5ac456102f
1 file changed
+73
-2
+73
-2
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user