From 563ffad3ab03349728831e8380fbb7bc489f6fde Mon Sep 17 00:00:00 2001 From: spinline Date: Tue, 3 Feb 2026 23:12:51 +0300 Subject: [PATCH] feat: add global stats broadcasting and statusbar integration --- backend/src/main.rs | 24 +++++++++++-- backend/src/sse.rs | 28 +++++++++++++-- frontend/src/components/layout/statusbar.rs | 40 ++++++++++++++++++--- frontend/src/store.rs | 11 +++++- shared/src/lib.rs | 14 ++++++-- 5 files changed, 105 insertions(+), 12 deletions(-) diff --git a/backend/src/main.rs b/backend/src/main.rs index e780da1..2b985c2 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -148,12 +148,19 @@ async fn main() { let mut previous_torrents: Vec = Vec::new(); loop { - match sse::fetch_torrents(&client).await { + // 1. Fetch Torrents + let torrents_result = sse::fetch_torrents(&client).await; + + // 2. Fetch Global Stats + let stats_result = sse::fetch_global_stats(&client).await; + + // Handle Torrents + match torrents_result { Ok(new_torrents) => { - // 1. Update latest state (always) + // Update latest state let _ = tx_clone.send(new_torrents.clone()); - // 2. Calculate Diff and Broadcasting + // Calculate Diff and Broadcasting let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() @@ -180,6 +187,17 @@ async fn main() { tracing::error!("Error fetching torrents in background: {}", e); } } + + // Handle Stats + match stats_result { + Ok(stats) => { + let _ = event_bus_tx.send(AppEvent::Stats(stats)); + } + Err(e) => { + tracing::warn!("Error fetching global stats: {}", e); + } + } + tokio::time::sleep(Duration::from_secs(1)).await; } }); diff --git a/backend/src/sse.rs b/backend/src/sse.rs index 72e4509..6f8492d 100644 --- a/backend/src/sse.rs +++ b/backend/src/sse.rs @@ -3,7 +3,7 @@ use crate::AppState; use axum::extract::State; use axum::response::sse::{Event, Sse}; use futures::stream::{self, Stream}; -use shared::{AppEvent, Torrent, TorrentStatus}; +use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus}; use std::convert::Infallible; use tokio_stream::StreamExt; @@ -52,7 +52,6 @@ fn from_rtorrent_row(row: Vec) -> Torrent { let is_hashing = parse_long(row.get(11)); let label_raw = parse_string(row.get(12)); - // Treat empty label as None let label = if label_raw.is_empty() { None } else { @@ -115,6 +114,31 @@ pub async fn fetch_torrents(client: &RtorrentClient) -> Result, Xml Ok(torrents) } +pub async fn fetch_global_stats(client: &RtorrentClient) -> Result { + // Parallel calls would be better but let's keep it simple sequential for now. + // NOTE: This adds 4 roundtrips per second. If this is too slow, we should use multicall via system.multicall (if supported) + // or just accept the overhead. Unix socket overhead is very low. + + // We ignore errors on individual stats to not break the whole loop, using defaults. + // But connection errors should propagate. + + let down_rate_str = client.call("throttle.global_down.rate", &[]).await?; + let up_rate_str = client.call("throttle.global_up.rate", &[]).await?; + let down_limit_str = client.call("throttle.global_down.max_rate", &[]).await?; + let up_limit_str = client.call("throttle.global_up.max_rate", &[]).await?; + + // Optionally get free space. "directory.default" then "d.free_space_path"?? No "get_directory_free_space" + // Let's skip free space for high frequency updates. + + Ok(GlobalStats { + down_rate: down_rate_str.parse().unwrap_or(0), + up_rate: up_rate_str.parse().unwrap_or(0), + down_limit: down_limit_str.parse().ok(), + up_limit: up_limit_str.parse().ok(), + free_space: None, + }) +} + pub async fn sse_handler( State(state): State, ) -> Sse>> { diff --git a/frontend/src/components/layout/statusbar.rs b/frontend/src/components/layout/statusbar.rs index 296eaeb..b29a99f 100644 --- a/frontend/src/components/layout/statusbar.rs +++ b/frontend/src/components/layout/statusbar.rs @@ -1,22 +1,54 @@ use leptos::*; +fn format_bytes(bytes: i64) -> String { + const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"]; + if bytes < 1024 { + return format!("{} B", bytes); + } + let i = (bytes as f64).log2().div_euclid(10.0) as usize; + format!( + "{:.1} {}", + (bytes as f64) / 1024_f64.powi(i as i32), + UNITS[i] + ) +} + +fn format_speed(bytes_per_sec: i64) -> String { + if bytes_per_sec == 0 { + return "0 B/s".to_string(); + } + format!("{}/s", format_bytes(bytes_per_sec)) +} + #[component] pub fn StatusBar() -> impl IntoView { + let store = use_context::().expect("store not provided"); + let stats = store.global_stats; let (theme_open, set_theme_open) = create_signal(false); view! {
-
+
- "0 KB/s" + {move || format_speed(stats.get().down_rate)} + 0 fallback=|| ()> + + {move || format!("(Limit: {})", format_speed(stats.get().down_limit.unwrap_or(0)))} + +
-
+
- "0 KB/s" + {move || format_speed(stats.get().up_rate)} + 0 fallback=|| ()> + + {move || format!("(Limit: {})", format_speed(stats.get().up_limit.unwrap_or(0)))} + +
diff --git a/frontend/src/store.rs b/frontend/src/store.rs index 5f5f9d8..9475094 100644 --- a/frontend/src/store.rs +++ b/frontend/src/store.rs @@ -1,7 +1,7 @@ use futures::StreamExt; use gloo_net::eventsource::futures::EventSource; use leptos::*; -use shared::{AppEvent, Torrent}; +use shared::{AppEvent, GlobalStats, Torrent}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FilterStatus { @@ -33,17 +33,20 @@ pub struct TorrentStore { pub torrents: RwSignal>, pub filter: RwSignal, pub search_query: RwSignal, + pub global_stats: RwSignal, } pub fn provide_torrent_store() { let torrents = create_rw_signal(Vec::::new()); let filter = create_rw_signal(FilterStatus::All); let search_query = create_rw_signal(String::new()); + let global_stats = create_rw_signal(GlobalStats::default()); let store = TorrentStore { torrents, filter, search_query, + global_stats, }; provide_context(store); @@ -91,9 +94,15 @@ pub fn provide_torrent_store() { if let Some(error_message) = update.error_message { t.error_message = error_message; } + if let Some(label) = update.label { + t.label = label; + } } }); } + AppEvent::Stats(stats) => { + global_stats.set(stats); + } } } } diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 70d2ae2..5fb50d3 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -14,7 +14,7 @@ pub struct Torrent { pub status: TorrentStatus, pub error_message: String, pub added_date: i64, - pub label: Option, // Added Label support + pub label: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)] @@ -35,6 +35,16 @@ pub enum AppEvent { timestamp: u64, }, Update(TorrentUpdate), + Stats(GlobalStats), +} + +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, Default)] +pub struct GlobalStats { + pub down_rate: i64, + pub up_rate: i64, + pub down_limit: Option, + pub up_limit: Option, + pub free_space: Option, } #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] @@ -49,7 +59,7 @@ pub struct TorrentUpdate { pub eta: Option, pub status: Option, pub error_message: Option, - pub label: Option, // Added Label update support + pub label: Option, } #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]