Replace Transmission with qBittorrent for torrent statistics
All checks were successful
Build and Release / build-and-release (push) Successful in 1m27s

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
This commit is contained in:
Christoffer Martinsson 2025-12-02 23:31:56 +01:00
parent 477724b4f4
commit 1cb6abf58a
5 changed files with 42 additions and 55 deletions

6
Cargo.lock generated
View File

@ -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",

View File

@ -1,6 +1,6 @@
[package]
name = "cm-dashboard-agent"
version = "0.1.245"
version = "0.1.246"
edition = "2021"
[dependencies]

View File

@ -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::Value> = 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;

View File

@ -1,6 +1,6 @@
[package]
name = "cm-dashboard"
version = "0.1.245"
version = "0.1.246"
edition = "2021"
[dependencies]

View File

@ -1,6 +1,6 @@
[package]
name = "cm-dashboard-shared"
version = "0.1.245"
version = "0.1.246"
edition = "2021"
[dependencies]