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:
@@ -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(); });
|
||||
|
||||
Reference in New Issue
Block a user