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:
Luna 2026-05-25 22:11:01 -07:00
parent 4b0e4b3b07
commit 9d414919cd
3 changed files with 107 additions and 1 deletions

View file

@ -1762,6 +1762,7 @@ impl App {
let mut create_clicked = false;
let mut to_rename: Option<(i64, String)> = None;
let mut to_delete: Option<(i64, String, usize)> = None;
let mut to_check: Option<i64> = None;
egui::Window::new("📁 Manage folders")
.open(&mut open)
.collapsible(false)
@ -1782,9 +1783,14 @@ impl App {
if ui.small_button("🗑").on_hover_text("Delete folder").clicked() {
to_delete = Some((f.id, f.name.clone(), member_count));
}
if ui.small_button("").on_hover_text("Rename folder").clicked() {
if ui.small_button("").on_hover_text("Rename (type new name in field below first)").clicked() {
to_rename = Some((f.id, f.name.clone()));
}
if member_count > 0
&& ui.small_button("").on_hover_text("Check every channel in this folder for new videos").clicked()
{
to_check = Some(f.id);
}
});
});
ui.separator();
@ -1830,6 +1836,23 @@ impl App {
self.status = format!("Type new name into the field below, then click ✏ on '{old_name}' again");
}
}
if let Some(folder_id) = to_check {
// Check every member channel for new videos, applying that
// channel's own DownloadOptions / quality override just like
// a scheduled re-check would.
let scheduled: Vec<(String, crate::download_options::DownloadOptions)> =
self.library.iter()
.filter(|ch| ch.folder_id == Some(folder_id))
.map(|ch| (crate::downloader::recheck_url(ch), ch.download_options.clone()))
.collect();
let count = scheduled.len();
for (url, opts) in scheduled {
let info = classify_url(&url);
let quality = opts.quality.unwrap_or(DownloadQuality::Best);
self.downloader.start(url, &info, true, quality, false, Some(&opts));
}
self.status = format!("Folder check: started {count} channel download{}", if count == 1 { "" } else { "s" });
}
if let Some((id, name, count)) = to_delete {
// Hard delete — member channels revert to Unfiled.
if let Err(e) = self.db.delete_folder(id) {