Subtitle settings: global [subtitles] config + per-channel overrides

Adds full subtitle control (backend + desktop UI; web UI follows).

## Model
- New [subtitles] config section: enabled / auto_generated / embed /
  format / langs. Sensible defaults (subs on, auto on, no embed/convert,
  all langs).
- DownloadOptions gains per-channel Option overrides: subtitles_enabled,
  subtitles_auto, subtitles_embed, subtitle_format (subtitle_langs
  already existed). None = defer to global.

## Resolver
- Downloader gains subtitle_defaults (the global section) + a single
  apply_subtitle_flags() that merges global + per-channel and emits the
  yt-dlp flags (--write-subs / --write-auto-subs / --sub-langs /
  --convert-subs / --embed-subs). Disabled = emit nothing.
- Replaces the hardcoded --write-subs --write-auto-subs at the main
  download + repair builders. Moved the --sub-langs emission out of
  DownloadOptions::apply into the resolver so global + override merge in
  one place.

## Desktop UI
- Settings screen: "Subtitles (global defaults)" section.
- Channel-options dialog: tri-state (Default/On/Off) combos for each
  override + a convert-format field.

## Wiring
- subtitle_defaults seeded at Downloader construction (app + web) and
  refreshed on settings save (desktop).

4 resolver tests cover global defaults, disabled, global format/embed/
langs, and per-channel override-wins. 84 pass.

Web UI rows are the remaining piece (next commit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-01 05:44:33 -07:00
parent 1e95102ec8
commit 0814ba0c3b
5 changed files with 282 additions and 23 deletions

View file

@ -20,6 +20,47 @@ pub struct Config {
pub web: WebSection,
#[serde(default)]
pub plex: PlexSection,
#[serde(default)]
pub subtitles: SubtitlesSection,
}
/// `[subtitles]` table — global subtitle download defaults. Per-channel
/// [`crate::download_options::DownloadOptions`] can override any of these.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SubtitlesSection {
/// Master switch. When false, no subtitle flags are emitted at all.
#[serde(default = "default_true")]
pub enabled: bool,
/// Include auto-generated (machine) captions via `--write-auto-subs`.
/// Off-by-default-ish content is noisy; keep it on globally but let
/// channels turn it off.
#[serde(default = "default_true")]
pub auto_generated: bool,
/// Embed subtitle tracks into the video container (`--embed-subs`),
/// in addition to writing sidecar files. Soft subs, toggleable in
/// the player.
#[serde(default)]
pub embed: bool,
/// Convert downloaded subtitles to this format (`--convert-subs`).
/// Empty = keep native. Common: "srt" (Plex-friendly), "vtt", "ass".
#[serde(default)]
pub format: String,
/// Comma-separated language codes (`--sub-langs`). Empty = all
/// available. e.g. "en", "en,ja", "en.*" for all English variants.
#[serde(default)]
pub langs: String,
}
impl Default for SubtitlesSection {
fn default() -> Self {
Self {
enabled: true,
auto_generated: true,
embed: false,
format: String::new(),
langs: String::new(),
}
}
}
/// `[backup]` table — where to store downloaded videos.
@ -49,6 +90,7 @@ pub struct BackupSection {
}
fn default_max_concurrent() -> usize { 3 }
fn default_true() -> bool { true }
/// `[player]` table — external player and browser cookie source.
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -178,6 +220,7 @@ impl Config {
scheduler: SchedulerSection::default(),
web: WebSection::default(),
plex: PlexSection::default(),
subtitles: SubtitlesSection::default(),
}
}
}