Channels load for every channel type; parallel saves setting (default 4)

Channels: one loader tries, in order, the Videos/Live/Shorts tabs (normal
channels), the uploads playlist (Topic channels), the Releases/Playlists tabs
expanded into their tracks (artist/music channels whose only content is albums,
e.g. Cathedral of Praise Worship), then the home page; bare names are resolved
to an id first. Nothing public → ok with a message, never a raw yt-dlp error.

Saves: SaveSlots caps concurrent offline saves (Settings → Offline cache →
Videos saved at once, 1–8, default 4); queued saves show "Waiting for a free
slot", and resumed saves now run up to the limit instead of one at a time.
This commit is contained in:
Jonathan Sykes
2026-10-03 02:04:27 +08:00
parent 71f1913d0b
commit 9ce9a96f38
2 changed files with 101 additions and 36 deletions

View File

@@ -270,6 +270,21 @@ const SearchLibrary = (() => {
// OPFS bridge wrappers — return the same shape as the Tauri cache_* commands
// so the rest of app.js works without changes.
// ---------- Save slots: how many videos save at once (Settings, default 4) ----------
const SaveSlots = (() => {
let active = 0;
const waiters = [];
const limit = () => Math.max(1, Math.min(8, Number(data.settings.parallelSaves) || 4));
function pump() {
while (waiters.length && active < limit()) { active++; waiters.shift()(); }
}
return {
acquire() { return new Promise((r) => { waiters.push(r); pump(); }); },
release() { active = Math.max(0, active - 1); pump(); },
pump,
};
})();
// ---------- Save queue: resumable saves survive reloads and dropped links ----------
// Every save is recorded here when it starts and removed when it finishes or
// fails for good. A save that only paused (partial kept on the device) stays
@@ -298,6 +313,7 @@ const SaveQueue = (() => {
function label(p) {
if (!p) return '⏳ Saving for offline…';
if (p.phase === 'waiting') return '⏳ Waiting for a free slot…';
const mb = (n) => (n / 1048576).toFixed(n >= 1048576 * 100 ? 0 : 1);
if (p.phase === 'preparing') return `⏳ Preparing on server… ${Math.round((p.elapsed || 0) / 1000)} s`;
if (p.phase === 'verifying') return '✓ Verifying…';
@@ -325,13 +341,14 @@ const SaveQueue = (() => {
if (running || !navigator.onLine) return;
running = true;
try {
const jobs = [];
for (const v of pending()) {
if (cachedIds.has(v.id)) { remove(v.id); continue; }
if (downloading.has(v.id)) continue;
if (Date.now() - v.at > MAX_AGE) { cancel(v.id); continue; }
await preload(v, { quiet: true }); // one at a time: the homelab link is the bottleneck
if (!navigator.onLine) break;
jobs.push(preload(v, { quiet: true })); // SaveSlots caps how many run at once
}
await Promise.all(jobs);
} finally { running = false; }
if (view.type === 'downloads') renderList();
}
@@ -594,6 +611,7 @@ const DEFAULT_SETTINGS = {
shuffle: false, // shuffle upcoming tracks when playing a list
theme: 'dark', // 'dark' | 'light' | 'contrast'
fontScale: 'normal', // 'small' | 'normal' | 'large' | 'xl'
parallelSaves: 4, // offline saves running at once (Settings → Offline cache)
density: 'comfortable', // 'comfortable' | 'compact'
perfMode: false, // disable heavy visual effects for speed
reduceMotion: false, // disable animations
@@ -1751,7 +1769,12 @@ async function preload(video, { quiet = false, mux = false } = {}) {
if (current && current.meta && current.meta.id === id) updateNowPlayingActions();
if (!quiet) toast(`Saving “${video.title}” for offline…`);
SaveQueue.add(video);
SaveQueue.progress(id, { phase: 'waiting' });
let slot = false;
try {
await SaveSlots.acquire();
slot = true;
SaveQueue.progress(id, null);
const res = await API.cacheDownload(id, { mux, onProgress: (p) => SaveQueue.progress(id, p) });
if (res && res.paused) {
// The partial stays on the device; SaveQueue picks it up again.
@@ -1772,6 +1795,7 @@ async function preload(video, { quiet = false, mux = false } = {}) {
SaveQueue.remove(id);
if (!quiet) toast('⚠ Saving not supported in this build.');
} finally {
if (slot) SaveSlots.release();
SaveQueue.progress(id, null);
downloading.delete(id);
downloadMeta.delete(id);
@@ -8937,6 +8961,13 @@ async function renderSettings() {
</span>
<input id="setAutoPreload" type="checkbox" ${data.settings.autoPreload ? 'checked' : ''} />
</label>
<label class="set-row">
<span>
Videos saved at once
<small>How many offline saves run in parallel; the rest wait their turn.</small>
</span>
${sel('setParallelSaves', String(Math.max(1, Math.min(8, Number(data.settings.parallelSaves) || 4))), [['1', '1'], ['2', '2'], ['3', '3'], ['4', '4 (default)'], ['6', '6'], ['8', '8']])}
</label>
<div class="set-row">
<span>Storage used</span>
<span id="cacheTotal" class="set-stat">…</span>
@@ -9084,6 +9115,7 @@ async function renderSettings() {
$('setTheme').addEventListener('change', (e) => { data.settings.theme = e.target.value; applyAppearance(); persist(); });
$('setLayout').addEventListener('change', (e) => { data.settings.layout = e.target.value; applyAppearance(); persist(); });
$('setFont').addEventListener('change', (e) => { data.settings.fontScale = e.target.value; applyAppearance(); persist(); });
$('setParallelSaves').addEventListener('change', (e) => { data.settings.parallelSaves = Number(e.target.value) || 4; persist(); SaveSlots.pump(); });
$('setDensity').addEventListener('change', (e) => { data.settings.density = e.target.value; applyAppearance(); persist(); });
$('setPerf').addEventListener('change', (e) => { data.settings.perfMode = e.target.checked; applyAppearance(); persist(); });
$('setMotion').addEventListener('change', (e) => { data.settings.reduceMotion = e.target.checked; applyAppearance(); persist(); });