1use super::completion::QueueRunSummary;
4use super::types::ShardInfo;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9pub struct DownloadSummary {
10 pub id: String,
12 pub display_name: String,
14 pub status: DownloadStatus,
16 pub position: u32,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub error: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub group_id: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub shard_info: Option<ShardInfo>,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum DownloadStatus {
33 Queued,
35 Downloading,
37 Completed,
39 Failed,
41 Cancelled,
43}
44
45impl DownloadStatus {
46 #[must_use]
48 pub const fn as_str(&self) -> &'static str {
49 match self {
50 Self::Queued => "queued",
51 Self::Downloading => "downloading",
52 Self::Completed => "completed",
53 Self::Failed => "failed",
54 Self::Cancelled => "cancelled",
55 }
56 }
57
58 #[must_use]
60 pub fn parse(s: &str) -> Self {
61 match s {
62 "downloading" => Self::Downloading,
63 "completed" => Self::Completed,
64 "failed" => Self::Failed,
65 "cancelled" => Self::Cancelled,
66 _ => Self::Queued,
68 }
69 }
70}
71
72#[derive(Clone, Debug, Serialize, Deserialize)]
87#[serde(tag = "type", rename_all = "snake_case")]
88pub enum DownloadEvent {
89 QueueSnapshot {
91 items: Vec<DownloadSummary>,
93 max_size: u32,
95 },
96
97 DownloadStarted {
99 id: String,
101 },
102
103 DownloadProgress {
105 id: String,
107 downloaded: u64,
109 total: u64,
111 speed_bps: f64,
113 eta_seconds: f64,
115 percentage: f64,
117 },
118
119 ShardProgress {
121 id: String,
123 shard_index: u32,
125 total_shards: u32,
127 shard_filename: String,
129 shard_downloaded: u64,
131 shard_total: u64,
133 aggregate_downloaded: u64,
135 aggregate_total: u64,
137 speed_bps: f64,
139 eta_seconds: f64,
141 percentage: f64,
143 },
144
145 DownloadCompleted {
147 id: String,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 message: Option<String>,
152 },
153
154 DownloadFailed {
156 id: String,
158 error: String,
160 },
161
162 DownloadCancelled {
164 id: String,
166 },
167
168 QueueRunComplete {
174 summary: QueueRunSummary,
176 },
177}
178
179impl DownloadEvent {
180 #[must_use]
182 pub const fn queue_snapshot(items: Vec<DownloadSummary>, max_size: u32) -> Self {
183 Self::QueueSnapshot { items, max_size }
184 }
185
186 pub fn started(id: impl Into<String>) -> Self {
188 Self::DownloadStarted { id: id.into() }
189 }
190
191 #[allow(clippy::cast_precision_loss)]
193 pub fn progress(id: impl Into<String>, downloaded: u64, total: u64, speed_bps: f64) -> Self {
194 let percentage = if total > 0 {
195 (downloaded as f64 / total as f64) * 100.0
196 } else {
197 0.0
198 };
199
200 let eta_seconds = if speed_bps > 0.0 && total > downloaded {
201 (total - downloaded) as f64 / speed_bps
202 } else {
203 0.0
204 };
205
206 Self::DownloadProgress {
207 id: id.into(),
208 downloaded,
209 total,
210 speed_bps,
211 eta_seconds,
212 percentage,
213 }
214 }
215
216 #[allow(clippy::too_many_arguments, clippy::cast_precision_loss)]
218 pub fn shard_progress(
219 id: impl Into<String>,
220 shard_index: u32,
221 total_shards: u32,
222 shard_filename: impl Into<String>,
223 shard_downloaded: u64,
224 shard_total: u64,
225 aggregate_downloaded: u64,
226 aggregate_total: u64,
227 speed_bps: f64,
228 ) -> Self {
229 let percentage = if aggregate_total > 0 {
230 (aggregate_downloaded as f64 / aggregate_total as f64) * 100.0
231 } else {
232 0.0
233 };
234
235 let eta_seconds = if speed_bps > 0.0 && aggregate_total > aggregate_downloaded {
236 (aggregate_total - aggregate_downloaded) as f64 / speed_bps
237 } else {
238 0.0
239 };
240
241 Self::ShardProgress {
242 id: id.into(),
243 shard_index,
244 total_shards,
245 shard_filename: shard_filename.into(),
246 shard_downloaded,
247 shard_total,
248 aggregate_downloaded,
249 aggregate_total,
250 speed_bps,
251 eta_seconds,
252 percentage,
253 }
254 }
255
256 pub fn completed(id: impl Into<String>, message: Option<impl Into<String>>) -> Self {
258 Self::DownloadCompleted {
259 id: id.into(),
260 message: message.map(Into::into),
261 }
262 }
263
264 pub fn failed(id: impl Into<String>, error: impl Into<String>) -> Self {
266 Self::DownloadFailed {
267 id: id.into(),
268 error: error.into(),
269 }
270 }
271
272 pub fn cancelled(id: impl Into<String>) -> Self {
274 Self::DownloadCancelled { id: id.into() }
275 }
276
277 pub const fn queue_run_complete(summary: QueueRunSummary) -> Self {
279 Self::QueueRunComplete { summary }
280 }
281
282 #[must_use]
284 pub fn id(&self) -> Option<&str> {
285 match self {
286 Self::QueueSnapshot { .. } | Self::QueueRunComplete { .. } => None,
287 Self::DownloadStarted { id }
288 | Self::DownloadProgress { id, .. }
289 | Self::ShardProgress { id, .. }
290 | Self::DownloadCompleted { id, .. }
291 | Self::DownloadFailed { id, .. }
292 | Self::DownloadCancelled { id } => Some(id),
293 }
294 }
295
296 #[must_use]
302 pub const fn event_name(&self) -> &'static str {
303 match self {
304 Self::QueueSnapshot { .. } => "download:queue_snapshot",
305 Self::DownloadStarted { .. } => "download:started",
306 Self::DownloadProgress { .. } | Self::ShardProgress { .. } => "download:progress",
307 Self::DownloadCompleted { .. } => "download:completed",
308 Self::DownloadFailed { .. } => "download:failed",
309 Self::DownloadCancelled { .. } => "download:cancelled",
310 Self::QueueRunComplete { .. } => "download:queue_run_complete",
311 }
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_progress_event_calculations() {
321 let event = DownloadEvent::progress("id", 500, 1000, 100.0);
322 match event {
323 DownloadEvent::DownloadProgress {
324 percentage,
325 eta_seconds,
326 ..
327 } => {
328 assert!((percentage - 50.0).abs() < 0.01);
329 assert!((eta_seconds - 5.0).abs() < 0.01);
330 }
331 _ => panic!("Expected DownloadProgress"),
332 }
333 }
334
335 #[test]
336 fn test_event_id_extraction() {
337 assert_eq!(DownloadEvent::started("test").id(), Some("test"));
338 assert_eq!(DownloadEvent::cancelled("test").id(), Some("test"));
339 assert!(DownloadEvent::queue_snapshot(vec![], 10).id().is_none());
340 }
341}