fix(torrent): use live peer telemetry in properties

- source the Properties peer card from live Aria2 status counts
- distinguish connected peers from unavailable peer details
- remove redundant peer-summary IPC and harden count parsing
- add responsive, accessible peer/seeder presentation and regressions
This commit is contained in:
NimBold
2026-08-15 06:26:25 +03:30
parent de41dd55d6
commit ca32b772a2
23 changed files with 204 additions and 252 deletions
-10
View File
@@ -346,16 +346,6 @@ pub struct TorrentPeerDiagnostics {
pub truncated: bool,
}
#[derive(Clone, Debug, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../../src/bindings/")]
pub struct TorrentPeerSummary {
#[ts(type = "number")]
pub total_peers: u32,
#[ts(type = "number")]
pub total_seeders: u32,
}
#[derive(Clone, Debug, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../../src/bindings/")]
+22 -23
View File
@@ -7470,17 +7470,6 @@ async fn get_torrent_peers(
state.queue_manager.get_aria2_torrent_peers(&id).await
}
#[tauri::command]
async fn get_torrent_peer_summary(
caller: tauri::WebviewWindow,
properties: tauri::State<'_, properties_window::PropertiesWindowRegistry>,
state: tauri::State<'_, AppState>,
id: String,
) -> Result<crate::ipc::TorrentPeerSummary, String> {
properties_window::ensure_properties_or_main(&caller, &properties, &id)?;
state.queue_manager.get_aria2_torrent_peer_summary(&id).await
}
#[tauri::command]
async fn get_torrent_availability(
caller: tauri::WebviewWindow,
@@ -11194,7 +11183,7 @@ mod tests {
observe_aria2_connections, observe_aria2_connections_with_epoch,
Aria2ConnectionObservation, Aria2ConnectionSample, Aria2RecoveryReason,
FrontendExitFlush,
aria2_active_connection_count,
aria2_active_connection_count, aria2_nonnegative_count,
parse_media_playlist_metadata,
normalize_media_connections,
validate_enqueue_url, validate_enqueue_uris, validate_keychain_grant_request_id,
@@ -11734,6 +11723,10 @@ mod tests {
0
);
assert_eq!(aria2_active_connection_count(&json!({})), 0);
assert_eq!(aria2_nonnegative_count(&json!({"numSeeders": "6"}), "numSeeders"), Some(6));
assert_eq!(aria2_nonnegative_count(&json!({"numSeeders": 6}), "numSeeders"), Some(6));
assert_eq!(aria2_nonnegative_count(&json!({"numSeeders": "-1"}), "numSeeders"), None);
assert_eq!(aria2_nonnegative_count(&json!({}), "numSeeders"), None);
}
#[test]
@@ -13940,17 +13933,23 @@ struct Aria2ConnectionSample<'a> {
now: Instant,
}
fn aria2_active_connection_count(status_info: &serde_json::Value) -> i32 {
fn aria2_count_value(value: &serde_json::Value) -> Option<i32> {
value
.as_str()
.and_then(|value| value.parse::<i64>().ok())
.or_else(|| value.as_i64())
.and_then(|value| i32::try_from(value).ok())
}
fn aria2_nonnegative_count(status_info: &serde_json::Value, key: &str) -> Option<i32> {
status_info
.get("connections")
.and_then(|value| {
value
.as_str()
.and_then(|value| value.parse::<i32>().ok())
.or_else(|| value.as_i64().and_then(|value| i32::try_from(value).ok()))
})
.get(key)
.and_then(aria2_count_value)
.filter(|value| *value >= 0)
.unwrap_or(0)
}
fn aria2_active_connection_count(status_info: &serde_json::Value) -> i32 {
aria2_nonnegative_count(status_info, "connections").unwrap_or(0)
}
const ARIA2_CONNECTION_RECOVERY_DELAY: Duration = Duration::from_secs(30);
@@ -14968,7 +14967,7 @@ pub fn run() {
let speed_bytes = status_info.get("downloadSpeed").and_then(|s| s.as_str()).unwrap_or("0").parse::<f64>().unwrap_or(0.0);
let uploaded_bytes = status_info.get("uploadLength").and_then(|s| s.as_str()).and_then(|value| value.parse::<u64>().ok());
let upload_speed_bytes = status_info.get("uploadSpeed").and_then(|s| s.as_str()).and_then(|value| value.parse::<f64>().ok());
let num_seeders = status_info.get("numSeeders").and_then(|s| s.as_str()).and_then(|value| value.parse::<i32>().ok());
let num_seeders = aria2_nonnegative_count(status_info, "numSeeders");
let is_seeder = status_info.get("seeder").is_some_and(|value| {
value.as_str() == Some("true") || value.as_bool() == Some(true)
});
@@ -15558,7 +15557,7 @@ pub fn run() {
authorize_keychain_access,
acknowledge_pairing_token_change,
check_file_exists, toggle_tray_icon, set_extension_pairing_token,
get_extension_server_port, set_extension_frontend_ready, ack_frontend_exit, ack_extension_download, set_concurrent_limit, set_queue_concurrency_limits, set_download_speed_limit, set_torrent_upload_limit, set_torrent_peer_options, get_torrent_peers, get_torrent_peer_summary, get_torrent_availability, get_torrent_file_progress, get_torrent_piece_progress, get_torrent_file_selection, set_torrent_file_selection, get_torrent_details, get_torrent_magnet_link, export_torrent_metadata, move_torrent_data, cancel_torrent_move_data, verify_torrent_data, get_torrent_web_seeds, set_torrent_web_seeds, set_torrent_max_open_files, set_torrent_overall_upload_limit, set_global_speed_limit, remove_download, get_download_primary_path,
get_extension_server_port, set_extension_frontend_ready, ack_frontend_exit, ack_extension_download, set_concurrent_limit, set_queue_concurrency_limits, set_download_speed_limit, set_torrent_upload_limit, set_torrent_peer_options, get_torrent_peers, get_torrent_availability, get_torrent_file_progress, get_torrent_piece_progress, get_torrent_file_selection, set_torrent_file_selection, get_torrent_details, get_torrent_magnet_link, export_torrent_metadata, move_torrent_data, cancel_torrent_move_data, verify_torrent_data, get_torrent_web_seeds, set_torrent_web_seeds, set_torrent_max_open_files, set_torrent_overall_upload_limit, set_global_speed_limit, remove_download, get_download_primary_path,
detach_download_for_reconfigure,
enqueue_download, enqueue_many, cancel_enqueue_generation, move_in_queue, move_many_in_queue, remove_from_queue, get_pending_order,
commands::reveal_in_file_manager, commands::open_downloaded_file,
+9 -49
View File
@@ -2734,19 +2734,6 @@ impl<R: tauri::Runtime> QueueManager<R> {
parse_torrent_peer_diagnostics(result)
}
/// Return only aggregate peer/seeder counts for the current Torrent GID.
/// No peer addresses, IDs, bitfields, or transfer rates cross the IPC
/// boundary.
pub async fn get_aria2_torrent_peer_summary(
&self,
id: &str,
) -> Result<crate::ipc::TorrentPeerSummary, String> {
let result = self
.get_aria2_torrent_peer_result(id, "peer summary")
.await?;
parse_torrent_peer_summary(result)
}
/// Compute bounded, anonymized swarm availability for the current
/// Torrent lifecycle. The raw local/peer bitfields are consumed in native
/// memory and never returned to the frontend.
@@ -5955,9 +5942,14 @@ pub(crate) fn parse_torrent_availability(
})
}
fn torrent_peer_summary_from_array(
struct TorrentPeerCounts {
total_peers: u32,
total_seeders: u32,
}
fn torrent_peer_counts_from_array(
peers: &[serde_json::Value],
) -> Result<crate::ipc::TorrentPeerSummary, String> {
) -> Result<TorrentPeerCounts, String> {
if peers.len() > MAX_TORRENT_PEER_RESPONSE {
return Err("aria2.getPeers returned too many peers".to_string());
}
@@ -5973,28 +5965,19 @@ fn torrent_peer_summary_from_array(
total_seeders = total_seeders.saturating_add(1);
}
}
Ok(crate::ipc::TorrentPeerSummary {
Ok(TorrentPeerCounts {
total_peers: u32::try_from(peers.len()).unwrap_or(u32::MAX),
total_seeders,
})
}
pub(crate) fn parse_torrent_peer_summary(
result: serde_json::Value,
) -> Result<crate::ipc::TorrentPeerSummary, String> {
let peers = result
.as_array()
.ok_or_else(|| "aria2.getPeers returned a non-array result".to_string())?;
torrent_peer_summary_from_array(peers)
}
pub(crate) fn parse_torrent_peer_diagnostics(
result: serde_json::Value,
) -> Result<crate::ipc::TorrentPeerDiagnostics, String> {
let peers = result
.as_array()
.ok_or_else(|| "aria2.getPeers returned a non-array result".to_string())?;
let summary = torrent_peer_summary_from_array(peers)?;
let summary = torrent_peer_counts_from_array(peers)?;
let mut sanitized = Vec::with_capacity(peers.len().min(MAX_TORRENT_PEER_DIAGNOSTICS));
for peer in peers.iter().take(MAX_TORRENT_PEER_DIAGNOSTICS) {
@@ -8344,28 +8327,10 @@ mod tests {
assert!(!serialized.contains("bitfield"));
}
#[test]
fn torrent_peer_summary_counts_all_seeders_without_returning_peer_data() {
let result = serde_json::json!([
{"ip": "192.0.2.10", "seeder": "true", "bitfield": "secret"},
{"ip": "192.0.2.11", "seeder": false},
{"ip": "192.0.2.12", "seeder": true}
]);
let summary = parse_torrent_peer_summary(result).unwrap();
assert_eq!(summary.total_peers, 3);
assert_eq!(summary.total_seeders, 2);
let serialized = serde_json::to_string(&summary).unwrap();
assert!(!serialized.contains("192.0.2."));
assert!(!serialized.contains("bitfield"));
}
#[test]
fn torrent_peer_diagnostics_reject_non_array_results() {
let error = parse_torrent_peer_diagnostics(serde_json::json!({"peers": []})).unwrap_err();
assert!(error.contains("non-array"));
let summary_error = parse_torrent_peer_summary(serde_json::json!({"peers": []})).unwrap_err();
assert!(summary_error.contains("non-array"));
}
#[test]
@@ -8375,11 +8340,6 @@ mod tests {
}, "not-a-peer"]))
.unwrap_err();
assert!(error.contains("malformed"));
let summary_error = parse_torrent_peer_summary(serde_json::json!([{
"seeder": true
}, "not-a-peer"]))
.unwrap_err();
assert!(summary_error.contains("malformed"));
}
fn test_torrent_progress_metadata() -> Vec<crate::ipc::TorrentFile> {