From 7a3ed179524e9811317c86da20ccba87eda2be01 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Tue, 2 Dec 2025 11:12:14 +0100 Subject: [PATCH] Add torrent statistics to transmission-vpn service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement aggregate torrent statistics display for transmission-vpn service via Transmission RPC API. Shows active torrent count and total download/upload speeds. Change VPN route label from "ip:" to "route:" for clarity. - Add get_transmission_stats() method to query Transmission RPC - Display format: "X active, ↓ MB/s, ↑ MB/s" - Update version to v0.1.244 --- Cargo.lock | 6 +-- agent/Cargo.toml | 2 +- agent/src/collectors/systemd.rs | 83 ++++++++++++++++++++++++++++++++- dashboard/Cargo.toml | 2 +- shared/Cargo.toml | 2 +- 5 files changed, 88 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5362c10..02fd954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.242" +version = "0.1.243" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.242" +version = "0.1.243" dependencies = [ "anyhow", "async-trait", @@ -325,7 +325,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.242" +version = "0.1.243" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 2f0733d..6a33697 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.243" +version = "0.1.244" edition = "2021" [dependencies] diff --git a/agent/src/collectors/systemd.rs b/agent/src/collectors/systemd.rs index 8c66146..8621f4f 100644 --- a/agent/src/collectors/systemd.rs +++ b/agent/src/collectors/systemd.rs @@ -164,7 +164,7 @@ impl SystemdCollector { let metrics = Vec::new(); sub_services.push(SubServiceData { - name: format!("ip: {}", external_ip), + name: format!("route: {}", external_ip), service_status: Status::Info, metrics, service_type: "vpn_route".to_string(), @@ -172,6 +172,19 @@ 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() { + let metrics = Vec::new(); + + sub_services.push(SubServiceData { + name: format!("{} active, ↓ {:.1} MB/s, ↑ {:.1} MB/s", active_count, download_mbps, upload_mbps), + service_status: Status::Info, + metrics, + service_type: "torrent_stats".to_string(), + }); + } + } + // Create complete service data let service_data = ServiceData { name: service_name.clone(), @@ -878,6 +891,74 @@ impl SystemdCollector { None } + + /// Get aggregate transmission 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() + .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, + }?; + + // 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 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); + + // Status codes: 0=stopped, 4=downloading, 6=seeding + // Count as active if downloading or seeding + if status_code == 4 || status_code == 6 { + active_count += 1; + } + + total_download_bps += rate_download; + total_upload_bps += rate_upload; + } + + // Convert bytes/s 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; + + Some((active_count, download_mbps, upload_mbps)) + } } #[async_trait] diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index 3a0d3bd..028e24a 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.243" +version = "0.1.244" edition = "2021" [dependencies] diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 4959f74..c0d8dc3 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.243" +version = "0.1.244" edition = "2021" [dependencies]