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:
parent
1e95102ec8
commit
0814ba0c3b
5 changed files with 282 additions and 23 deletions
90
src/app.rs
90
src/app.rs
|
|
@ -226,9 +226,24 @@ struct ChannelOptionsForm {
|
|||
date_after: String,
|
||||
match_filter: String,
|
||||
subtitle_langs: String, // comma-separated
|
||||
// Per-channel subtitle override tri-states: 0 = use global default,
|
||||
// 1 = force on, 2 = force off. Map to Option<bool> on save.
|
||||
subs_enabled_idx: usize,
|
||||
subs_auto_idx: usize,
|
||||
subs_embed_idx: usize,
|
||||
subtitle_format: String, // "" = global default
|
||||
extra_args: String, // one per line
|
||||
}
|
||||
|
||||
/// Map an `Option<bool>` override to the tri-state combo index.
|
||||
fn tri_to_idx(v: Option<bool>) -> usize {
|
||||
match v { None => 0, Some(true) => 1, Some(false) => 2 }
|
||||
}
|
||||
/// Inverse of [`tri_to_idx`].
|
||||
fn idx_to_tri(i: usize) -> Option<bool> {
|
||||
match i { 1 => Some(true), 2 => Some(false), _ => None }
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(cc: &eframe::CreationContext<'_>, tray: Option<crate::tray::TrayHandle>) -> Self {
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
|
|
@ -292,6 +307,13 @@ impl App {
|
|||
let use_bundled_ytdlp = config.backup.use_bundled_ytdlp;
|
||||
let use_pot_provider = config.backup.use_pot_provider;
|
||||
let browser = config.player.browser.clone();
|
||||
// Build the downloader up front so we can seed its subtitle
|
||||
// defaults from config before it goes into the struct literal.
|
||||
let mut downloader = Downloader::new(
|
||||
channels_root.clone(), browser.clone(), max_concurrent,
|
||||
use_bundled_ytdlp, use_pot_provider,
|
||||
);
|
||||
downloader.subtitle_defaults = config.subtitles.clone();
|
||||
let config_bind = config.web.bind.clone();
|
||||
let password_set = db.get_setting("password_hash").ok().flatten().is_some();
|
||||
let plex_path_str = config.plex.library_path
|
||||
|
|
@ -361,7 +383,7 @@ impl App {
|
|||
note_buffer: String::new(),
|
||||
note_target: None,
|
||||
search: String::new(),
|
||||
downloader: Downloader::new(channels_root, browser, max_concurrent, use_bundled_ytdlp, use_pot_provider),
|
||||
downloader,
|
||||
show_downloads: false,
|
||||
current_screen: Screen::Library,
|
||||
dl_url: String::new(),
|
||||
|
|
@ -448,6 +470,10 @@ impl App {
|
|||
date_after: opts.date_after.clone().unwrap_or_default(),
|
||||
match_filter: opts.match_filter.clone().unwrap_or_default(),
|
||||
subtitle_langs: opts.subtitle_langs.join(", "),
|
||||
subs_enabled_idx: tri_to_idx(opts.subtitles_enabled),
|
||||
subs_auto_idx: tri_to_idx(opts.subtitles_auto),
|
||||
subs_embed_idx: tri_to_idx(opts.subtitles_embed),
|
||||
subtitle_format: opts.subtitle_format.clone().unwrap_or_default(),
|
||||
extra_args: opts.extra_args.join("\n"),
|
||||
}
|
||||
}
|
||||
|
|
@ -480,6 +506,10 @@ impl App {
|
|||
match_filter: trim_opt(&f.match_filter),
|
||||
subtitle_langs: f.subtitle_langs.split(',')
|
||||
.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(),
|
||||
subtitles_enabled: idx_to_tri(f.subs_enabled_idx),
|
||||
subtitles_auto: idx_to_tri(f.subs_auto_idx),
|
||||
subtitles_embed: idx_to_tri(f.subs_embed_idx),
|
||||
subtitle_format: trim_opt(&f.subtitle_format),
|
||||
extra_args: f.extra_args.lines()
|
||||
.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(),
|
||||
}
|
||||
|
|
@ -1946,6 +1976,36 @@ impl App {
|
|||
.hint_text("en, ja"),
|
||||
);
|
||||
|
||||
// Per-channel subtitle overrides. "Default" defers to the
|
||||
// global [subtitles] config; the other choices force the
|
||||
// behavior for this channel only.
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("Subtitle overrides (Default = use global setting):").small().weak());
|
||||
let form = &mut self.channel_options_form;
|
||||
let tri_combo = |ui: &mut egui::Ui, id: &str, label: &str, idx: &mut usize| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(label);
|
||||
egui::ComboBox::from_id_salt(id)
|
||||
.selected_text(match *idx { 1 => "On", 2 => "Off", _ => "Default" })
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(idx, 0, "Default");
|
||||
ui.selectable_value(idx, 1, "On");
|
||||
ui.selectable_value(idx, 2, "Off");
|
||||
});
|
||||
});
|
||||
};
|
||||
tri_combo(ui, "subs_enabled", "Download subtitles", &mut form.subs_enabled_idx);
|
||||
tri_combo(ui, "subs_auto", "Auto-generated captions", &mut form.subs_auto_idx);
|
||||
tri_combo(ui, "subs_embed", "Embed into video", &mut form.subs_embed_idx);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Convert format");
|
||||
ui.add(
|
||||
egui::TextEdit::singleline(&mut form.subtitle_format)
|
||||
.desired_width(120.0)
|
||||
.hint_text("global (srt/vtt/ass)"),
|
||||
).on_hover_text("Blank = use the global setting. e.g. srt for Plex.");
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
ui.label("Extra yt-dlp args (one per line):");
|
||||
ui.add(
|
||||
|
|
@ -2522,6 +2582,33 @@ impl App {
|
|||
ui.end_row();
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.heading("Subtitles (global defaults)");
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Applied to every download. Individual channels can override these \
|
||||
in their Channel options dialog.",
|
||||
).small().weak(),
|
||||
);
|
||||
let subs = &mut self.config.subtitles;
|
||||
ui.checkbox(&mut subs.enabled, "Download subtitles");
|
||||
ui.add_enabled_ui(subs.enabled, |ui| {
|
||||
ui.checkbox(&mut subs.auto_generated, "Include auto-generated (machine) captions");
|
||||
ui.checkbox(&mut subs.embed, "Embed subtitles into the video file")
|
||||
.on_hover_text("--embed-subs: soft subs toggleable in the player, in addition to sidecar files.");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Languages (comma sep, blank = all):");
|
||||
ui.add(egui::TextEdit::singleline(&mut subs.langs).desired_width(160.0).hint_text("en, ja"));
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Convert to format (blank = native):");
|
||||
ui.add(egui::TextEdit::singleline(&mut subs.format).desired_width(100.0).hint_text("srt"))
|
||||
.on_hover_text("--convert-subs. srt is the most player/Plex-compatible.");
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.heading("Backup");
|
||||
|
|
@ -2713,6 +2800,7 @@ impl App {
|
|||
self.downloader.max_concurrent = self.config.backup.max_concurrent;
|
||||
self.downloader.use_bundled_ytdlp = self.config.backup.use_bundled_ytdlp;
|
||||
self.downloader.use_pot_provider = self.config.backup.use_pot_provider;
|
||||
self.downloader.subtitle_defaults = self.config.subtitles.clone();
|
||||
if dir_changed {
|
||||
self.channels_root = new_dir.clone();
|
||||
self.library_root = new_dir
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,24 @@ pub struct DownloadOptions {
|
|||
#[serde(default)]
|
||||
pub subtitle_langs: Vec<String>,
|
||||
|
||||
/// Per-channel subtitle overrides. Each `None` falls back to the
|
||||
/// global `[subtitles]` config. Resolved in [`crate::downloader`]'s
|
||||
/// subtitle-flag builder, not here, because `apply()` doesn't have
|
||||
/// the global config in scope.
|
||||
///
|
||||
/// - `subtitles_enabled`: master on/off for this channel.
|
||||
/// - `subtitles_auto`: include auto-generated captions.
|
||||
/// - `subtitles_embed`: embed into the container.
|
||||
/// - `subtitle_format`: convert to this format (empty string = native).
|
||||
#[serde(default)]
|
||||
pub subtitles_enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub subtitles_auto: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub subtitles_embed: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub subtitle_format: Option<String>,
|
||||
|
||||
/// Raw passthrough — every entry is appended as a separate argument to
|
||||
/// yt-dlp. Lets users access any flag we haven't exposed yet. Equivalent
|
||||
/// to Tartube's `extra_cmd_string`.
|
||||
|
|
@ -131,9 +149,10 @@ impl DownloadOptions {
|
|||
cmd.arg("--match-filter").arg(filter);
|
||||
}
|
||||
}
|
||||
if !self.subtitle_langs.is_empty() {
|
||||
cmd.arg("--sub-langs").arg(self.subtitle_langs.join(","));
|
||||
}
|
||||
// Subtitle flags (langs / write / auto / embed / convert) are
|
||||
// emitted by the downloader's subtitle resolver, which merges
|
||||
// these per-channel overrides with the global [subtitles] config.
|
||||
// apply() handles everything else.
|
||||
for arg in &self.extra_args {
|
||||
if !arg.is_empty() {
|
||||
cmd.arg(arg);
|
||||
|
|
@ -189,17 +208,9 @@ mod tests {
|
|||
assert_eq!(args, vec!["--limit-rate", "500K"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtitle_langs_join_with_commas() {
|
||||
let mut cmd = Command::new("yt-dlp");
|
||||
DownloadOptions {
|
||||
subtitle_langs: vec!["en".into(), "ja".into(), "es".into()],
|
||||
..Default::default()
|
||||
}
|
||||
.apply(&mut cmd);
|
||||
let args: Vec<String> = cmd.get_args().map(|a| a.to_string_lossy().into_owned()).collect();
|
||||
assert_eq!(args, vec!["--sub-langs", "en,ja,es"]);
|
||||
}
|
||||
// Subtitle-flag emission moved to the downloader's resolver (it merges
|
||||
// these per-channel langs with the global [subtitles] config). The
|
||||
// resolver's behavior is tested in downloader.rs.
|
||||
|
||||
#[test]
|
||||
fn skip_auth_check_emits_extractor_arg() {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
//! | Flag | Purpose |
|
||||
//! |---|---|
|
||||
//! | `--cookies cookies.txt` | Pass browser cookies for age-gated/member videos |
|
||||
//! | `--write-subs --write-auto-subs` | Download subtitles alongside the video |
|
||||
//! | `--write-subs` (+ optionally `--write-auto-subs` / `--sub-langs` / `--convert-subs` / `--embed-subs`) | Subtitles, per the `[subtitles]` config + per-channel overrides |
|
||||
//! | `--write-thumbnail` | Download channel/video thumbnails |
|
||||
//! | `--write-description` | Save video description as a sidecar `.description` file |
|
||||
//! | `--write-info-json` | Save full metadata as a `.info.json` sidecar |
|
||||
|
|
@ -191,6 +191,10 @@ pub struct Downloader {
|
|||
/// Running bgutil-pot server child. Lazily spawned in [`Self::start`]
|
||||
/// on the first job after the flag turns on; killed in [`Drop`].
|
||||
pot_server: Option<std::process::Child>,
|
||||
/// Global subtitle defaults from `[subtitles]` config. Per-channel
|
||||
/// [`DownloadOptions`] override individual fields. Set by the app /
|
||||
/// web layer at construction and on settings save.
|
||||
pub subtitle_defaults: crate::config::SubtitlesSection,
|
||||
}
|
||||
|
||||
impl Downloader {
|
||||
|
|
@ -210,6 +214,46 @@ impl Downloader {
|
|||
use_bundled_ytdlp,
|
||||
use_pot_provider,
|
||||
pot_server: None,
|
||||
subtitle_defaults: crate::config::SubtitlesSection::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve global `[subtitles]` config + per-channel overrides into
|
||||
/// the yt-dlp subtitle flag set, appending to `cmd`.
|
||||
///
|
||||
/// Resolution: a per-channel `Some` wins over the global default for
|
||||
/// each field. When subtitles are disabled (globally or per-channel),
|
||||
/// nothing is emitted — yt-dlp then writes no subs.
|
||||
fn apply_subtitle_flags(&self, cmd: &mut Command, opts: Option<&DownloadOptions>) {
|
||||
let g = &self.subtitle_defaults;
|
||||
// Per-channel override-or-global for each knob.
|
||||
let enabled = opts.and_then(|o| o.subtitles_enabled).unwrap_or(g.enabled);
|
||||
if !enabled { return; }
|
||||
let auto = opts.and_then(|o| o.subtitles_auto).unwrap_or(g.auto_generated);
|
||||
let embed = opts.and_then(|o| o.subtitles_embed).unwrap_or(g.embed);
|
||||
// Format: per-channel Some(non-empty) wins; else global.
|
||||
let format = opts
|
||||
.and_then(|o| o.subtitle_format.as_deref())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or(g.format.as_str());
|
||||
// Langs: per-channel list wins; else global comma string.
|
||||
let langs: String = match opts {
|
||||
Some(o) if !o.subtitle_langs.is_empty() => o.subtitle_langs.join(","),
|
||||
_ => g.langs.clone(),
|
||||
};
|
||||
|
||||
cmd.arg("--write-subs");
|
||||
if auto {
|
||||
cmd.arg("--write-auto-subs");
|
||||
}
|
||||
if !langs.is_empty() {
|
||||
cmd.arg("--sub-langs").arg(langs);
|
||||
}
|
||||
if !format.is_empty() {
|
||||
cmd.arg("--convert-subs").arg(format);
|
||||
}
|
||||
if embed {
|
||||
cmd.arg("--embed-subs");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -455,9 +499,7 @@ impl Downloader {
|
|||
let mut cmd = self.ytdlp_cmd();
|
||||
cmd.arg("--newline").arg("--no-color");
|
||||
self.apply_cookie_flags(&mut cmd);
|
||||
cmd.arg("--write-subs")
|
||||
.arg("--write-auto-subs")
|
||||
.arg("--write-thumbnail")
|
||||
cmd.arg("--write-thumbnail")
|
||||
.arg("--write-description")
|
||||
.arg("--write-info-json")
|
||||
// Don't write channel/playlist-level metafiles (avatar, info.json,
|
||||
|
|
@ -492,6 +534,10 @@ impl Downloader {
|
|||
.arg(archive_path.display().to_string());
|
||||
self.apply_impersonation(info.platform, &mut cmd);
|
||||
self.apply_platform_extras(info.platform, &mut cmd);
|
||||
// Subtitle flags: global [subtitles] config merged with per-channel
|
||||
// overrides. Done here (not in opts.apply) so it works even when a
|
||||
// channel has no options row.
|
||||
self.apply_subtitle_flags(&mut cmd, channel_options);
|
||||
// Per-channel option overrides win when present — they're applied
|
||||
// last so a `--limit-rate` / `--match-filter` / passthrough arg from
|
||||
// the channel settings takes priority over the global defaults.
|
||||
|
|
@ -522,9 +568,10 @@ impl Downloader {
|
|||
self.apply_cookie_flags(&mut cmd);
|
||||
cmd.arg("--write-thumbnail")
|
||||
.arg("--write-info-json")
|
||||
.arg("--write-description")
|
||||
.arg("--write-subs")
|
||||
.arg("--write-auto-subs");
|
||||
.arg("--write-description");
|
||||
// Repair has no channel-options context, so subtitles use the
|
||||
// global [subtitles] defaults.
|
||||
self.apply_subtitle_flags(&mut cmd, None);
|
||||
// `repair()` rebuilds a YouTube watch URL from a stored video ID, so
|
||||
// the source platform is always YouTube here regardless of where the
|
||||
// original video lives on disk.
|
||||
|
|
@ -904,5 +951,74 @@ mod tests {
|
|||
let input = "[download] 47.2% of 100MiB";
|
||||
assert_eq!(redact_sensitive(input, abs), input);
|
||||
}
|
||||
|
||||
// ── Subtitle resolver ────────────────────────────────────────────────
|
||||
fn dl_with_subs(subs: crate::config::SubtitlesSection) -> Downloader {
|
||||
let mut d = Downloader::new(
|
||||
std::path::PathBuf::from("/tmp"), "none".into(), 1, false, false);
|
||||
d.subtitle_defaults = subs;
|
||||
d
|
||||
}
|
||||
fn args_of(d: &Downloader, opts: Option<&DownloadOptions>) -> Vec<String> {
|
||||
let mut cmd = Command::new("yt-dlp");
|
||||
d.apply_subtitle_flags(&mut cmd, opts);
|
||||
cmd.get_args().map(|a| a.to_string_lossy().into_owned()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subs_global_defaults_emit_write_and_auto() {
|
||||
let d = dl_with_subs(crate::config::SubtitlesSection::default());
|
||||
let args = args_of(&d, None);
|
||||
assert!(args.contains(&"--write-subs".to_string()));
|
||||
assert!(args.contains(&"--write-auto-subs".to_string()));
|
||||
// No langs/format/embed by default.
|
||||
assert!(!args.contains(&"--embed-subs".to_string()));
|
||||
assert!(!args.iter().any(|a| a == "--convert-subs"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subs_disabled_emits_nothing() {
|
||||
let mut g = crate::config::SubtitlesSection::default();
|
||||
g.enabled = false;
|
||||
let d = dl_with_subs(g);
|
||||
assert!(args_of(&d, None).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subs_global_format_embed_langs() {
|
||||
let g = crate::config::SubtitlesSection {
|
||||
enabled: true, auto_generated: false, embed: true,
|
||||
format: "srt".into(), langs: "en,ja".into(),
|
||||
};
|
||||
let d = dl_with_subs(g);
|
||||
let args = args_of(&d, None);
|
||||
assert!(args.contains(&"--write-subs".to_string()));
|
||||
assert!(!args.contains(&"--write-auto-subs".to_string())); // auto off
|
||||
assert!(args.windows(2).any(|w| w == ["--sub-langs", "en,ja"]));
|
||||
assert!(args.windows(2).any(|w| w == ["--convert-subs", "srt"]));
|
||||
assert!(args.contains(&"--embed-subs".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subs_per_channel_overrides_global() {
|
||||
// Global: enabled+auto. Channel: force OFF.
|
||||
let d = dl_with_subs(crate::config::SubtitlesSection::default());
|
||||
let opts = DownloadOptions { subtitles_enabled: Some(false), ..Default::default() };
|
||||
assert!(args_of(&d, Some(&opts)).is_empty());
|
||||
|
||||
// Global: auto on. Channel: auto off + embed on + own langs.
|
||||
let opts2 = DownloadOptions {
|
||||
subtitles_auto: Some(false),
|
||||
subtitles_embed: Some(true),
|
||||
subtitle_langs: vec!["en".into()],
|
||||
subtitle_format: Some("vtt".into()),
|
||||
..Default::default()
|
||||
};
|
||||
let args = args_of(&d, Some(&opts2));
|
||||
assert!(!args.contains(&"--write-auto-subs".to_string()));
|
||||
assert!(args.contains(&"--embed-subs".to_string()));
|
||||
assert!(args.windows(2).any(|w| w == ["--sub-langs", "en"]));
|
||||
assert!(args.windows(2).any(|w| w == ["--convert-subs", "vtt"]));
|
||||
}
|
||||
// URL classification tests live in `platform` now — see its tests module.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2304,13 +2304,14 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) {
|
|||
let watched = db.get_watched().unwrap_or_default();
|
||||
let positions = db.get_positions().unwrap_or_default();
|
||||
|
||||
let downloader = Downloader::new(
|
||||
let mut downloader = Downloader::new(
|
||||
channels_root.clone(),
|
||||
config.player.browser.clone(),
|
||||
config.backup.max_concurrent,
|
||||
config.backup.use_bundled_ytdlp,
|
||||
config.backup.use_pot_provider,
|
||||
);
|
||||
downloader.subtitle_defaults = config.subtitles.clone();
|
||||
let music_root = downloader.music_root();
|
||||
let _ = std::fs::create_dir_all(&music_root);
|
||||
let transcode = AtomicBool::new(config.web.transcode);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue