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.
This commit is contained in:
@@ -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::<u64>() { cfg.publisher.deluge.poll_interval_secs = n; }
|
||||
}
|
||||
"deluge.labels" => {
|
||||
if let Ok(v) = serde_json::from_str::<Vec<String>>(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::<u64>() { cfg.publisher.qbittorrent.poll_interval_secs = n; }
|
||||
}
|
||||
"qbittorrent.categories" => {
|
||||
if let Ok(v) = serde_json::from_str::<Vec<String>>(value) {
|
||||
cfg.publisher.qbittorrent.categories = v;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ pub fn router() -> Router<AppState> {
|
||||
.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))
|
||||
}
|
||||
|
||||
|
||||
@@ -221,6 +221,93 @@ async fn render(state: &AppState, params: &PageParams) -> anyhow::Result<Html<St
|
||||
api_key = esc(&cfg.tmdb.api_key),
|
||||
);
|
||||
|
||||
// ── Deluge ────────────────────────────────────────────────────────────────
|
||||
let d = &cfg.publisher.deluge;
|
||||
let deluge_enabled = if d.enabled { " checked" } else { "" };
|
||||
let deluge_labels = d.labels.join(", ");
|
||||
let deluge_section = format!(
|
||||
r#"<div class="card">
|
||||
<div class="card-title">Deluge <span class="badge badge-warn" style="font-size:10px;vertical-align:middle">restart required</span></div>
|
||||
<form method="POST" action="/ui/settings/deluge">
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Enabled</label>
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" name="enabled" value="true"{deluge_enabled}>
|
||||
Poll Deluge for completed downloads
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Web UI URL
|
||||
<span class="setting-hint">Enable the Web UI plugin in Deluge, then log in once in a browser to set a password. Default port is 8112.</span>
|
||||
</label>
|
||||
<input type="text" name="url" value="{d_url}" placeholder="http://127.0.0.1:8112" style="width:280px">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Password
|
||||
<span class="setting-hint">The password you set when you first logged into the Deluge web UI</span>
|
||||
</label>
|
||||
<input type="password" name="password" value="{d_pass}" placeholder="••••••••" style="width:220px">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Poll interval (seconds)</label>
|
||||
<input type="number" name="poll_interval_secs" value="{d_poll}" min="10" style="width:100px">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Labels to watch
|
||||
<span class="setting-hint">Comma-separated Deluge Label plugin labels. Leave blank to watch all completed torrents.</span>
|
||||
</label>
|
||||
<input type="text" name="labels" value="{deluge_labels}" placeholder="publish-nostr" style="width:320px">
|
||||
</div>
|
||||
<div style="margin-top:.75rem">
|
||||
<button type="submit" class="btn-accent">Save Deluge settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>"#,
|
||||
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#"<div class="card">
|
||||
<div class="card-title">qBittorrent <span class="badge badge-warn" style="font-size:10px;vertical-align:middle">restart required</span></div>
|
||||
<form method="POST" action="/ui/settings/qbittorrent">
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Web UI URL</label>
|
||||
<input type="text" name="url" value="{qb_url}" placeholder="http://127.0.0.1:8080" style="width:280px">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Username</label>
|
||||
<input type="text" name="username" value="{qb_user}" placeholder="admin" style="width:180px">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Password</label>
|
||||
<input type="password" name="password" value="{qb_pass}" placeholder="••••••••" style="width:220px">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Poll interval (seconds)</label>
|
||||
<input type="number" name="poll_interval_secs" value="{qb_poll}" min="10" style="width:100px">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">Categories to watch
|
||||
<span class="setting-hint">Comma-separated qBittorrent category names. Leave blank to watch all completed torrents.</span>
|
||||
</label>
|
||||
<input type="text" name="categories" value="{qb_cats}" placeholder="publish-nostr" style="width:320px">
|
||||
</div>
|
||||
<div style="margin-top:.75rem">
|
||||
<button type="submit" class="btn-accent">Save qBittorrent settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>"#,
|
||||
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() {
|
||||
"<tr><td colspan='4' class='empty'>No API keys yet — create one below</td></tr>".into()
|
||||
@@ -295,6 +382,11 @@ function copyKey() {
|
||||
<div class="section-title">API Keys</div>
|
||||
{apikeys_section}
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Torrent clients</div>
|
||||
{deluge_section}
|
||||
{qbittorrent_section}
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Network</div>
|
||||
{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<AppState>,
|
||||
Form(form): Form<DelugeForm>,
|
||||
) -> Redirect {
|
||||
let enabled = form.enabled == "true";
|
||||
let labels: Vec<String> = 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<AppState>,
|
||||
Form(form): Form<QbittorrentForm>,
|
||||
) -> Redirect {
|
||||
let categories: Vec<String> = 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();
|
||||
|
||||
Reference in New Issue
Block a user