From 1cb6abf58a40830a0f3757d8fb733464f32e53af Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Tue, 2 Dec 2025 23:31:56 +0100 Subject: [PATCH] Replace Transmission with qBittorrent for torrent statistics Update collector to use qBittorrent Web API instead of Transmission RPC. Query qBittorrent through VPN namespace using existing passwordless sudo permissions for ip netns exec commands. - Change service name from transmission-vpn to openvpn-vpn-download - Replace get_transmission_stats() with get_qbittorrent_stats() - Use curl through VPN namespace to access qBittorrent API at localhost:8080 - Parse qBittorrent JSON response for state, dlspeed, upspeed - Count active torrents (downloading, uploading, stalledDL, stalledUP) - Update version to v0.1.246 --- Cargo.lock | 6 +-- agent/Cargo.toml | 2 +- agent/src/collectors/systemd.rs | 85 ++++++++++++++------------------- dashboard/Cargo.toml | 2 +- shared/Cargo.toml | 2 +- 5 files changed, 42 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b5c4f1..998e135 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.244" +version = "0.1.245" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.244" +version = "0.1.245" dependencies = [ "anyhow", "async-trait", @@ -325,7 +325,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.244" +version = "0.1.245" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index f111c9b..3b8468c 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.245" +version = "0.1.246" edition = "2021" [dependencies] diff --git a/agent/src/collectors/systemd.rs b/agent/src/collectors/systemd.rs index c72aa97..4580874 100644 --- a/agent/src/collectors/systemd.rs +++ b/agent/src/collectors/systemd.rs @@ -167,8 +167,8 @@ impl SystemdCollector { } } - if service_name == "transmission-vpn" && status_info.active_state == "active" { - if let Some((active_count, download_mbps, upload_mbps)) = self.get_transmission_stats() { + if service_name == "openvpn-vpn-download" && status_info.active_state == "active" { + if let Some((active_count, download_mbps, upload_mbps)) = self.get_qbittorrent_stats() { let metrics = Vec::new(); sub_services.push(SubServiceData { @@ -887,68 +887,55 @@ impl SystemdCollector { None } - /// Get aggregate transmission torrent statistics + /// Get aggregate qBittorrent torrent statistics /// Returns: (active_count, download_mbps, upload_mbps) - fn get_transmission_stats(&self) -> Option<(u32, f32, f32)> { - let rpc_url = "http://localhost:9091/transmission/rpc"; - - // Create HTTP client with timeout - let client = reqwest::blocking::Client::builder() - .timeout(std::time::Duration::from_secs(5)) - .build() + fn get_qbittorrent_stats(&self) -> Option<(u32, f32, f32)> { + // Query qBittorrent API through VPN namespace + let output = Command::new("timeout") + .args(&[ + "5", + "sudo", + "ip", + "netns", + "exec", + "vpn", + "curl", + "-s", + "--max-time", + "4", + "http://localhost:8080/api/v2/torrents/info" + ]) + .output() .ok()?; - // First request to get session ID (transmission requires this for CSRF protection) - let session_id = match client.post(rpc_url).send() { - Ok(resp) => { - resp.headers() - .get("X-Transmission-Session-Id") - .and_then(|v| v.to_str().ok()) - .map(|s| s.to_string()) - } - Err(_) => return None, - }?; + if !output.status.success() { + return None; + } - // Request torrent list with session ID - let request_body = serde_json::json!({ - "method": "torrent-get", - "arguments": { - "fields": ["status", "rateDownload", "rateUpload"] - } - }); - - let response = client - .post(rpc_url) - .header("X-Transmission-Session-Id", session_id) - .json(&request_body) - .send() - .ok()?; - - let json: serde_json::Value = response.json().ok()?; - - // Parse torrent data and calculate aggregates - let torrent_list = json["arguments"]["torrents"].as_array()?; + let output_str = String::from_utf8_lossy(&output.stdout); + let torrents: Vec = serde_json::from_str(&output_str).ok()?; let mut active_count = 0u32; let mut total_download_bps = 0.0f64; let mut total_upload_bps = 0.0f64; - for torrent in torrent_list { - let status_code = torrent["status"].as_i64().unwrap_or(0); - let rate_download = torrent["rateDownload"].as_f64().unwrap_or(0.0); - let rate_upload = torrent["rateUpload"].as_f64().unwrap_or(0.0); + for torrent in torrents { + let state = torrent["state"].as_str().unwrap_or(""); + let dlspeed = torrent["dlspeed"].as_f64().unwrap_or(0.0); + let upspeed = torrent["upspeed"].as_f64().unwrap_or(0.0); - // Status codes: 0=stopped, 4=downloading, 6=seeding - // Count as active if downloading or seeding - if status_code == 4 || status_code == 6 { + // States: downloading, uploading, stalledDL, stalledUP, queuedDL, queuedUP, pausedDL, pausedUP + // Count as active if downloading or uploading (seeding) + if state.contains("downloading") || state.contains("uploading") || + state == "stalledDL" || state == "stalledUP" { active_count += 1; } - total_download_bps += rate_download; - total_upload_bps += rate_upload; + total_download_bps += dlspeed; + total_upload_bps += upspeed; } - // Convert bytes/s to MB/s + // qBittorrent returns bytes/s, convert to MB/s let download_mbps = (total_download_bps / 1024.0 / 1024.0) as f32; let upload_mbps = (total_upload_bps / 1024.0 / 1024.0) as f32; diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index c84991b..b82bf6a 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.245" +version = "0.1.246" edition = "2021" [dependencies] diff --git a/shared/Cargo.toml b/shared/Cargo.toml index c3919ac..ec17813 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.245" +version = "0.1.246" edition = "2021" [dependencies]