Add bundled yt-dlp, music library, Plex metadata, sort by date; security + perf hardening

Bug fixes
- Desktop auto-rescan: snapshot was_running before check_notifications
  updates prev_job_states, so the post-download rescan actually fires.
- web::run shutdown: replace `let _ = tx;` (which dropped the sender
  immediately and short-circuited the shutdown select!) with
  std::mem::forget(tx); add an idle fallback so the `-> !` is honored.
- /api/preview now resolves the bundled yt-dlp path and extends PATH
  for deno discovery, matching the rest of the downloader.
- Scheduler interval clamped to >=1h on read so a manually-edited
  config.toml with 0 hours can't trigger every tick.
- maintenance::is_within canonicalizes the parent + filename when the
  target is missing; remove_files treats NotFound as success.

Security
- Session tokens stored with issued-at Instant and pruned past 30 day
  TTL on every touch (was: unbounded HashSet).
- Login rate-limited per source IP: 5 failures → 60s lockout (429).
- Secure cookie flag added when X-Forwarded-Proto: https is present.
- 4 MiB body size cap via DefaultBodyLimit.
- Security headers middleware: CSP, X-Content-Type-Options,
  X-Frame-Options, Referrer-Policy.
- JS safeUrl() defangs javascript:/vbscript:/data:text URLs before
  interpolation into src= attributes.
- Bundled yt-dlp install verifies SHA-256 against the release's
  SHA2-256SUMS; deno hash printed for visual inspection. Install also
  reports byte counts every 3s so users see live progress.
- yt-offline.db chmod 0600 at open time on Unix.

New features
- Bundled yt-dlp + deno mode: settings toggle (System/Bundled) plus
  one-click Install/Update from settings. Ensures executable bit on
  every launch in case the install chmod was missed.
- Download queue with configurable max_concurrent (1–10); pending
  jobs surfaced in both UIs.
- Music download mode: --extract-audio into music/<artist>/, separate
  sidebar entry, /api/music + /music-files/ endpoints, inline <audio>.
- Download quality selector: Best / 1080p / 720p / 480p / 360p.
- Sort videos by Newest / Oldest (upload_date from info.json with
  release_date fallback).
- Plex metadata: per-episode .nfo (title, season/episode, aired,
  runtime, plot from .description), <stem>-thumb.jpg symlink, and
  show-level tvshow.nfo.
- yt-dlp retry/throttle defaults: --retries 30 --fragment-retries 30
  --retry-sleep linear=1:30:2 --sleep-requests 1 to ride out the
  "Connection reset by peer" failures.

Performance
- password_required cached in AtomicBool; refreshed only on password
  change. Eliminates a SQLite query per static-file fetch.
- Job::log → VecDeque for O(1) front-pop instead of O(n) drain.
- Library scan parallelized across cores via stdlib parallel_map.
- Web UI poll: 600ms while active, 5s when idle, 2s after errors.
- Music scan now recursive (Artist/Album/Track.opus).
- percent_encode_segment uses write! to avoid per-byte String allocs.

Code quality
- 27 unit tests covering parse_progress, parse_stem, extract_after,
  detect_url_kind, srt_to_vtt, count_cookies, percent_encode_segment,
  bind_mode_of, hash_password, lang_label, is_within, sanitize,
  year_of, aired_date, xml_escape.
- Unified library::find_video + Channel::all_videos() iterator,
  replacing three duplicated linear searches.
- Downloader::browser wired to --cookies-from-browser as a fallback
  when no cookies.txt is present.
- mpv detection now matches the binary basename, not substring.
- Settings modal scroll fix; web settings expose all new fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-05-23 04:54:26 -07:00
parent 1d72069913
commit 59996735f3
10 changed files with 2300 additions and 156 deletions

View file

@ -18,14 +18,27 @@ pub struct Config {
pub scheduler: SchedulerSection,
#[serde(default)]
pub web: WebSection,
#[serde(default)]
pub plex: PlexSection,
}
/// `[backup]` table — where to store downloaded videos.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BackupSection {
pub directory: PathBuf,
/// Maximum simultaneous yt-dlp processes. Extra downloads queue and start
/// automatically when a slot opens. Set to 0 for no limit (not recommended).
#[serde(default = "default_max_concurrent")]
pub max_concurrent: usize,
/// If true, use the bundled yt-dlp + deno binaries managed by yt-offline
/// (installed under `~/.local/share/yt-offline/bin/`). If false, use the
/// `yt-dlp` found on the system PATH.
#[serde(default)]
pub use_bundled_ytdlp: bool,
}
fn default_max_concurrent() -> usize { 3 }
/// `[player]` table — external player and browser cookie source.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PlayerSection {
@ -71,6 +84,14 @@ impl Default for SchedulerSection {
}
}
/// `[plex]` table — Plex-compatible TV-show symlink library.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct PlexSection {
/// Directory where the Plex symlink tree is written.
/// Leave unset to disable Plex library generation.
pub library_path: Option<PathBuf>,
}
/// `[web]` table — built-in HTTP server settings.
///
/// `source_url` is **required for AGPL §13 compliance**: set it to a URL
@ -125,11 +146,16 @@ impl Config {
/// Construct a minimal default config pointing `backup.directory` at `dir`.
pub fn default_with_dir(dir: PathBuf) -> Self {
Self {
backup: BackupSection { directory: dir },
backup: BackupSection {
directory: dir,
max_concurrent: default_max_concurrent(),
use_bundled_ytdlp: false,
},
player: PlayerSection::default(),
ui: UiSection::default(),
scheduler: SchedulerSection::default(),
web: WebSection::default(),
plex: PlexSection::default(),
}
}
}