irontide_core/web_seed_stats.rs
1//! Per-URL stats for web-seed sources (BEP 17 + BEP 19).
2//!
3//! Lives in `irontide-core` because [`crate::FastResumeData`] persists a
4//! `HashMap<String, WebSeedStats>` so stats survive app restart (M178
5//! Tension-1). `irontide-session` re-exports this type to keep its public
6//! surface stable.
7
8use serde::{Deserialize, Serialize};
9
10/// Coarse state of a single web-seed source.
11///
12/// `Idle` — no successful chunk yet observed (cold-start before any progress).
13/// `Active` — at least one successful chunk landed; currently downloading.
14/// `Errored` — the most recent attempt failed; [`WebSeedStats::last_error`]
15/// has the message. Transitions back to `Active` on recovery, but
16/// `last_error` PERSISTS so users can see "currently working but flaked
17/// recently" in the GUI (M178 Issue 2.2 / D-eng-8).
18#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
19pub enum WebSeedState {
20 /// No successful chunk yet observed.
21 #[default]
22 Idle,
23 /// At least one successful chunk landed; currently downloading.
24 Active,
25 /// Most recent attempt failed; `last_error` is populated.
26 Errored,
27}
28
29/// Per-URL stats for a single web-seed source, surfaced to the qBt v2
30/// `/api/v2/torrents/webseeds` endpoint and the GUI's HTTP Sources tab.
31///
32/// `consecutive_failures` resets to zero on a successful chunk; within a
33/// failure run it monotonically increments. `last_error` PERSISTS through
34/// the Errored→Active transition so the GUI can show "currently active,
35/// last failed with X" — intentional per Issue 2.2 / D-eng-8.
36///
37/// `next_retry_unix_secs` is forward-compatible with M186's BEP 17/19
38/// retry-with-backoff logic (D-eng-9 / Issue 2.3); always `None` in M178.
39#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
40pub struct WebSeedStats {
41 /// Web-seed URL (BEP 19 url-list or BEP 17 httpseeds entry).
42 pub url: String,
43 /// Coarse current state. See [`WebSeedState`].
44 #[serde(default)]
45 pub state: WebSeedState,
46 /// Cumulative bytes downloaded from this URL since first observation.
47 #[serde(default)]
48 pub downloaded_bytes: u64,
49 /// Rate at the most recent emission, computed over the throttle window.
50 #[serde(default)]
51 pub last_rate_bps: u64,
52 /// Most recently observed error message; persists through recovery
53 /// (Issue 2.2 / D-eng-8).
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub last_error: Option<String>,
56 /// Consecutive failures since the last success. Resets to zero on
57 /// a successful chunk; monotonically increments within a failure run.
58 #[serde(default)]
59 pub consecutive_failures: u32,
60 /// Unix timestamp of the most recent emission (success or failure).
61 #[serde(default)]
62 pub last_attempt_unix_secs: u64,
63 /// M186 forward-compat (D-eng-9): when set, the unix timestamp at
64 /// which BEP 17/19 retry-with-backoff will next attempt this URL.
65 /// Always `None` in M178; populated once the retry logic ships.
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub next_retry_unix_secs: Option<u64>,
68}