From 6cb94285679186d82256ad4565f3ed08b3b3788e Mon Sep 17 00:00:00 2001 From: enki Date: Tue, 19 May 2026 21:24:41 -0700 Subject: [PATCH] add Deluge and qBittorrent sections to Settings page Both torrent clients now configurable from /ui/settings with enabled toggle, URL, password, poll interval, and labels/categories fields. Settings persist to DB and apply on next restart. --- src/db/queries.rs | 22 +++++++ src/ui/mod.rs | 2 + src/ui/settings.rs | 151 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) diff --git a/src/db/queries.rs b/src/db/queries.rs index c7fa420..cc420b8 100644 --- a/src/db/queries.rs +++ b/src/db/queries.rs @@ -1193,6 +1193,28 @@ pub async fn apply_settings_overrides(pool: &SqlitePool, cfg: &mut crate::config } "tmdb.enabled" => cfg.tmdb.enabled = value == "true", "tmdb.api_key" => cfg.tmdb.api_key = value.clone(), + "deluge.enabled" => cfg.publisher.deluge.enabled = value == "true", + "deluge.url" => cfg.publisher.deluge.url = value.clone(), + "deluge.password" => cfg.publisher.deluge.password = value.clone(), + "deluge.poll_interval_secs" => { + if let Ok(n) = value.parse::() { cfg.publisher.deluge.poll_interval_secs = n; } + } + "deluge.labels" => { + if let Ok(v) = serde_json::from_str::>(value) { + cfg.publisher.deluge.labels = v; + } + } + "qbittorrent.url" => cfg.publisher.qbittorrent.url = value.clone(), + "qbittorrent.username" => cfg.publisher.qbittorrent.username = value.clone(), + "qbittorrent.password" => cfg.publisher.qbittorrent.password = value.clone(), + "qbittorrent.poll_interval_secs" => { + if let Ok(n) = value.parse::() { cfg.publisher.qbittorrent.poll_interval_secs = n; } + } + "qbittorrent.categories" => { + if let Ok(v) = serde_json::from_str::>(value) { + cfg.publisher.qbittorrent.categories = v; + } + } _ => {} } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 5897300..b4a000f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -33,6 +33,8 @@ pub fn router() -> Router { .route("/ui/settings/tmdb", post(settings::save_tmdb)) .route("/ui/settings/apikeys/create", post(settings::create_api_key)) .route("/ui/settings/apikeys/delete", post(settings::delete_api_key)) + .route("/ui/settings/deluge", post(settings::save_deluge)) + .route("/ui/settings/qbittorrent", post(settings::save_qbittorrent)) .route("/torrent/{info_hash}", get(torrent_blob_handler)) } diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 231ea1a..feb285b 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -221,6 +221,93 @@ async fn render(state: &AppState, params: &PageParams) -> anyhow::Result +
Deluge restart required
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+"#, + d_url = esc(&d.url), + d_pass = esc(&d.password), + d_poll = d.poll_interval_secs, + ); + + // ── qBittorrent ─────────────────────────────────────────────────────────── + let qb = &cfg.publisher.qbittorrent; + let qb_cats = qb.categories.join(", "); + let qbittorrent_section = format!( + r#"
+
qBittorrent restart required
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
"#, + qb_url = esc(&qb.url), + qb_user = esc(&qb.username), + qb_pass = esc(&qb.password), + qb_poll = qb.poll_interval_secs, + ); + // ── API Keys ────────────────────────────────────────────────────────────── let key_rows: String = if api_keys.is_empty() { "No API keys yet — create one below".into() @@ -295,6 +382,11 @@ function copyKey() {
API Keys
{apikeys_section} +
+
Torrent clients
+ {deluge_section} + {qbittorrent_section} +
Network
{network_section} @@ -467,6 +559,65 @@ pub async fn delete_api_key( Redirect::to("/ui/settings?msg=Key+revoked") } +#[derive(Deserialize)] +pub struct DelugeForm { + #[serde(default)] + pub enabled: String, + #[serde(default)] + pub url: String, + #[serde(default)] + pub password: String, + pub poll_interval_secs: u64, + #[serde(default)] + pub labels: String, +} + +pub async fn save_deluge( + State(state): State, + Form(form): Form, +) -> Redirect { + let enabled = form.enabled == "true"; + let labels: Vec = form.labels.split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + let _ = db::set_setting(&state.pool, "deluge.enabled", if enabled { "true" } else { "false" }).await; + let _ = db::set_setting(&state.pool, "deluge.url", &form.url).await; + let _ = db::set_setting(&state.pool, "deluge.password", &form.password).await; + let _ = db::set_setting(&state.pool, "deluge.poll_interval_secs", &form.poll_interval_secs.to_string()).await; + let _ = db::set_setting(&state.pool, "deluge.labels", &serde_json::to_string(&labels).unwrap_or_default()).await; + Redirect::to("/ui/settings?msg=Deluge+settings+saved") +} + +#[derive(Deserialize)] +pub struct QbittorrentForm { + #[serde(default)] + pub url: String, + #[serde(default)] + pub username: String, + #[serde(default)] + pub password: String, + pub poll_interval_secs: u64, + #[serde(default)] + pub categories: String, +} + +pub async fn save_qbittorrent( + State(state): State, + Form(form): Form, +) -> Redirect { + let categories: Vec = form.categories.split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + let _ = db::set_setting(&state.pool, "qbittorrent.url", &form.url).await; + let _ = db::set_setting(&state.pool, "qbittorrent.username", &form.username).await; + let _ = db::set_setting(&state.pool, "qbittorrent.password", &form.password).await; + let _ = db::set_setting(&state.pool, "qbittorrent.poll_interval_secs", &form.poll_interval_secs.to_string()).await; + let _ = db::set_setting(&state.pool, "qbittorrent.categories", &serde_json::to_string(&categories).unwrap_or_default()).await; + Redirect::to("/ui/settings?msg=qBittorrent+settings+saved") +} + fn generate_key() -> String { use rand::Rng; let bytes: [u8; 32] = rand::thread_rng().gen();