From a8f59fe8547961da8c2ffce99ef7147d72b2578a Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 1 Jun 2026 06:07:19 -0700 Subject: [PATCH] Subtitle settings: web UI (global section + per-channel overrides) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the subtitle feature started in 0814ba0 (backend + desktop). ## Global settings (web Settings modal) - New "Subtitles (global defaults)" section: enable toggle that reveals auto-captions / embed / languages / convert-format controls. - SettingsPayload gains the 5 subtitle fields; get_settings reads them from [subtitles] config, post_settings writes them back, saves config, and pushes the live value onto downloader.subtitle_defaults. ## Per-channel overrides (channel-options dialog) - Tri-state selects (Default / On / Off) for download-subtitles, auto-captions, embed; a convert-format text field. "Default" defers to the global setting. - triSelect() / triValue() helpers map Option ⇄ select value. readChannelOptionsForm includes the four new fields; they serialize straight onto DownloadOptions via serde (post_channel_options already takes the whole struct). ## Verified end-to-end (live server) - Global: GET defaults → POST (auto off, embed on, langs=en, format=srt) → echoed back → written to config.toml [subtitles] → persists across restart. - Per-channel: POST overrides → read back exactly; all-default body hits the is_empty() delete path and clears the row. 84 tests pass. Co-Authored-By: Claude Opus 4.7 --- src/web.rs | 32 +++++++++++++++++++++ src/web_ui/index.html | 65 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/web.rs b/src/web.rs index db2e5b1..138ba8f 100644 --- a/src/web.rs +++ b/src/web.rs @@ -354,6 +354,19 @@ struct SettingsPayload { /// Whether the bgutil-pot binary is installed on disk (sent by server only). #[serde(skip_deserializing, default)] pot_provider_installed: bool, + /// Global subtitle defaults (the `[subtitles]` config section). + /// Round-trips on both GET and POST. Per-channel overrides live in + /// each channel's DownloadOptions, not here. + #[serde(default)] + subtitles_enabled: bool, + #[serde(default)] + subtitles_auto: bool, + #[serde(default)] + subtitles_embed: bool, + #[serde(default)] + subtitle_format: String, + #[serde(default)] + subtitle_langs: String, } #[derive(Serialize, Deserialize, Clone)] @@ -1206,6 +1219,7 @@ async fn get_settings(State(state): State>) -> impl IntoResponse { let max_concurrent = cfg.backup.max_concurrent; let use_bundled_ytdlp = cfg.backup.use_bundled_ytdlp; let use_pot_provider = cfg.backup.use_pot_provider; + let subs = cfg.subtitles.clone(); drop(cfg); let scheduler_next_check_secs = if scheduler_enabled { @@ -1237,6 +1251,11 @@ async fn get_settings(State(state): State>) -> impl IntoResponse { bundled_ytdlp_installed: crate::ytdlp_bin::bundled_installed(), use_pot_provider, pot_provider_installed: crate::pot_provider::installed(), + subtitles_enabled: subs.enabled, + subtitles_auto: subs.auto_generated, + subtitles_embed: subs.embed, + subtitle_format: subs.format, + subtitle_langs: subs.langs, }) } @@ -1270,6 +1289,12 @@ async fn post_settings( } cfg.backup.use_bundled_ytdlp = body.use_bundled_ytdlp; cfg.backup.use_pot_provider = body.use_pot_provider; + // Global subtitle defaults. + cfg.subtitles.enabled = body.subtitles_enabled; + cfg.subtitles.auto_generated = body.subtitles_auto; + cfg.subtitles.embed = body.subtitles_embed; + cfg.subtitles.format = body.subtitle_format.trim().to_string(); + cfg.subtitles.langs = body.subtitle_langs.trim().to_string(); if let Err(e) = cfg.save(&state.config_path) { return (StatusCode::INTERNAL_SERVER_ERROR, format!("save failed: {e}")).into_response(); @@ -1283,6 +1308,7 @@ async fn post_settings( let max_concurrent = cfg.backup.max_concurrent; let use_bundled_ytdlp = cfg.backup.use_bundled_ytdlp; let use_pot_provider = cfg.backup.use_pot_provider; + let subs = cfg.subtitles.clone(); drop(cfg); // Apply the new concurrency limit and binary choices to the live downloader. @@ -1293,6 +1319,7 @@ async fn post_settings( } dl.use_bundled_ytdlp = use_bundled_ytdlp; dl.use_pot_provider = use_pot_provider; + dl.subtitle_defaults = subs.clone(); } if let Some(new_pwd) = &body.new_download_password { @@ -1342,6 +1369,11 @@ async fn post_settings( bundled_ytdlp_installed: crate::ytdlp_bin::bundled_installed(), use_pot_provider, pot_provider_installed: crate::pot_provider::installed(), + subtitles_enabled: subs.enabled, + subtitles_auto: subs.auto_generated, + subtitles_embed: subs.embed, + subtitle_format: subs.format, + subtitle_langs: subs.langs, }).into_response() } diff --git a/src/web_ui/index.html b/src/web_ui/index.html index e50282b..1332088 100644 --- a/src/web_ui/index.html +++ b/src/web_ui/index.html @@ -882,6 +882,24 @@ function rebuildChannelUrl(ch){ async function downloadChannelByIdx(i){await downloadChannel(rebuildChannelUrl(library[i]))} /* ── Per-channel download options ──────────────────────────────── */ +// Tri-state select for an Option override. `v` is true / false / +// null|undefined (= use global default). Returns the ` + +`` + +`` + +`` + +``; +} +// Read a tri-state select back into an Option: "" → null, "on" → +// true, "off" → false. +function triValue(id){ + const v=document.getElementById(id)?.value; + return v==='on'?true:v==='off'?false:null; +} async function openChannelOptions(idx){ const ch=library[idx]; if(!ch)return; const platform=ch.platform, handle=ch.name; @@ -930,6 +948,15 @@ async function openChannelOptions(idx){
+
Subtitle overrides — "Default" uses the global setting; the others force the behavior for this channel.
+
+ ${triSelect('opt-subs-enabled',opts.subtitles_enabled)}
+
+ ${triSelect('opt-subs-auto',opts.subtitles_auto)}
+
+ ${triSelect('opt-subs-embed',opts.subtitles_embed)}
+
+