Per-folder "Check all" action + DB backup download
Two small follow-ups to the folder hierarchy that round out the folder-as-a-profile workflow and give the user a one-click way to keep an offsite copy of their library state. Per-folder check - New `POST /api/folders/:id/check` mirrors the existing /api/scheduler/run endpoint but scoped to one folder's members. Each channel's stored DownloadOptions (quality / rate limit / match-filter / extra args) applies just like a scheduled re-check. Returns 409 when downloads are already running. - Web folder-manager modal: each non-empty folder gets a "⬇ Check" button next to Rename / Delete. Status line reports the count of channels queued. - Desktop folder-manager window: same — a small ⬇ icon-button per row, visible only for folders with at least one member. Same options- honouring re-check path as the right-click "Check for new videos" action. - This effectively gives us "Profiles" lite: organize channels into a folder, then trigger a batch re-check whenever you want. DB backup - New `GET /api/backup/db` reads `<channels_root>/yt-offline.db` and streams it back with `Content-Disposition: attachment` so the browser downloads instead of rendering. Filename includes the unix timestamp so downloads don't clobber. Returns 404 when running in in-memory mode (no DB file to dump). - We deliberately don't bundle config.toml + cookies.txt here: config is short to recreate and cookies.txt carries live session credentials that shouldn't fly over the wire unprompted. - Web settings modal gains a "⬇ Download library snapshot" button in a new Backup section, with a hint explaining what's covered (watched / favourites / bookmarks / waiting / channel-options / folders). 55 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
4b0e4b3b07
commit
9d414919cd
3 changed files with 107 additions and 1 deletions
|
|
@ -690,6 +690,7 @@ async function openFolderManager(){
|
|||
const memberCount=library.filter(c=>c.folder_id===f.id).length;
|
||||
return `<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--border)">
|
||||
<span style="flex:1">📁 ${esc(f.name)} <span style="color:var(--muted);font-size:11px">(${memberCount} channel${memberCount===1?'':'s'})</span></span>
|
||||
${memberCount>0?`<button onclick="checkFolder(${f.id},'${esc(f.name)}',this)" title="Check every channel in this folder for new videos">⬇ Check</button>`:''}
|
||||
<button onclick="renameFolderPrompt(${f.id},'${esc(f.name)}')">Rename</button>
|
||||
<button onclick="deleteFolderConfirm(${f.id},'${esc(f.name)}',${memberCount})" style="color:#f87171">Delete</button>
|
||||
</div>`;
|
||||
|
|
@ -731,6 +732,16 @@ async function renameFolderPrompt(id,oldName){
|
|||
openFolderManager();
|
||||
}catch(e){setStatus('Error: '+e.message)}
|
||||
}
|
||||
async function checkFolder(id,name,btn){
|
||||
btn.disabled=true;
|
||||
try{
|
||||
const r=await fetch(`/api/folders/${id}/check`,{method:'POST'});
|
||||
const t=await r.text();
|
||||
if(!r.ok&&r.status!==409)throw new Error(t);
|
||||
setStatus(r.status===409?`${name}: downloads already running`:`${name}: ${t}`);
|
||||
}catch(e){setStatus('Error: '+e.message)}
|
||||
finally{btn.disabled=false}
|
||||
}
|
||||
async function deleteFolderConfirm(id,name,memberCount){
|
||||
const msg=memberCount>0
|
||||
? `Delete folder "${name}"? Its ${memberCount} channel${memberCount===1?'':'s'} will revert to "Unfiled".`
|
||||
|
|
@ -1072,6 +1083,12 @@ async function openSettings(){
|
|||
<span id="plex-status" style="font-size:11px;color:var(--muted)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<hr style="border-color:var(--border);margin:12px 0">
|
||||
<div style="font-weight:700;margin-bottom:8px">Backup</div>
|
||||
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:6px">
|
||||
<div class="settings-hint">Saves the SQLite file holding your watched / favourites / bookmarks / waiting / channel-options / folders. Restore by replacing <code>yt-offline.db</code> in the channels directory.</div>
|
||||
<button onclick="downloadBackupDb()">⬇ Download library snapshot</button>
|
||||
</div>
|
||||
${srcRow}
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:12px">
|
||||
${logoutBtn}
|
||||
|
|
@ -1113,6 +1130,12 @@ async function clearCookies(btn){
|
|||
}catch(e){setStatus('Error: '+e.message)}finally{btn.disabled=false}
|
||||
}
|
||||
|
||||
function downloadBackupDb(){
|
||||
// Open in a new tab so the auth cookie rides along; the server sets
|
||||
// Content-Disposition so the browser triggers a download instead of
|
||||
// rendering the SQLite bytes inline.
|
||||
window.open('/api/backup/db','_blank');
|
||||
}
|
||||
async function generatePlex(btn){
|
||||
const path=document.getElementById('cf-plex-path')?.value.trim();
|
||||
if(!path){document.getElementById('plex-status').textContent='Set a path first';return}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue