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 #[serde(skip_serializing_if = "Option::is_none")]
103 shard_index: Option<u32>,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 total_shards: Option<u32>,
107 },
108
109 DownloadProgress {
111 id: String,
113 downloaded: u64,
115 total: u64,
117 speed_bps: f64,
119 eta_seconds: f64,
121 percentage: f64,
123 },
124
125 ShardProgress {
127 id: String,
129 shard_index: u32,
131 total_shards: u32,
133 shard_filename: String,
135 shard_downloaded: u64,
137 shard_total: u64,
139 aggregate_downloaded: u64,
141 aggregate_total: u64,
143 speed_bps: f64,
145 eta_seconds: f64,
147 percentage: f64,
149 },
150
151 DownloadCompleted {
153 id: String,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 message: Option<String>,
158 },
159
160 DownloadFailed {
162 id: String,
164 error: String,
166 },
167
168 DownloadCancelled {
170 id: String,
172 },
173
174 QueueRunComplete {
180 summary: QueueRunSummary,
182 },
183}
184
185impl DownloadEvent {
186 #[must_use]
188 pub const fn queue_snapshot(items: Vec<DownloadSummary>, max_size: u32) -> Self {
189 Self::QueueSnapshot { items, max_size }
190 }
191
192 pub fn started(id: impl Into<String>) -> Self {
194 Self::DownloadStarted {
195 id: id.into(),
196 shard_index: None,
197 total_shards: None,
198 }
199 }
200
201 pub fn started_shard(id: impl Into<String>, shard_index: u32, total_shards: u32) -> Self {
203 Self::DownloadStarted {
204 id: id.into(),
205 shard_index: Some(shard_index),
206 total_shards: Some(total_shards),
207 }
208 }
209
210 #[allow(clippy::cast_precision_loss)]
212 pub fn progress(id: impl Into<String>, downloaded: u64, total: u64, speed_bps: f64) -> Self {
213 let percentage = if total > 0 {
214 (downloaded as f64 / total as f64) * 100.0
215 } else {
216 0.0
217 };
218
219 let eta_seconds = if speed_bps > 0.0 && total > downloaded {
220 (total - downloaded) as f64 / speed_bps
221 } else {
222 0.0
223 };
224
225 Self::DownloadProgress {
226 id: id.into(),
227 downloaded,
228 total,
229 speed_bps,
230 eta_seconds,
231 percentage,
232 }
233 }
234
235 #[allow(clippy::too_many_arguments, clippy::cast_precision_loss)]
237 pub fn shard_progress(
238 id: impl Into<String>,
239 shard_index: u32,
240 total_shards: u32,
241 shard_filename: impl Into<String>,
242 shard_downloaded: u64,
243 shard_total: u64,
244 aggregate_downloaded: u64,
245 aggregate_total: u64,
246 speed_bps: f64,
247 ) -> Self {
248 let percentage = if aggregate_total > 0 {
249 (aggregate_downloaded as f64 / aggregate_total as f64) * 100.0
250 } else {
251 0.0
252 };
253
254 let eta_seconds = if speed_bps > 0.0 && aggregate_total > aggregate_downloaded {
255 (aggregate_total - aggregate_downloaded) as f64 / speed_bps
256 } else {
257 0.0
258 };
259
260 Self::ShardProgress {
261 id: id.into(),
262 shard_index,
263 total_shards,
264 shard_filename: shard_filename.into(),
265 shard_downloaded,
266 shard_total,
267 aggregate_downloaded,
268 aggregate_total,
269 speed_bps,
270 eta_seconds,
271 percentage,
272 }
273 }
274
275 pub fn completed(id: impl Into<String>, message: Option<impl Into<String>>) -> Self {
277 Self::DownloadCompleted {
278 id: id.into(),
279 message: message.map(Into::into),
280 }
281 }
282
283 pub fn failed(id: impl Into<String>, error: impl Into<String>) -> Self {
285 Self::DownloadFailed {
286 id: id.into(),
287 error: error.into(),
288 }
289 }
290
291 pub fn cancelled(id: impl Into<String>) -> Self {
293 Self::DownloadCancelled { id: id.into() }
294 }
295
296 pub const fn queue_run_complete(summary: QueueRunSummary) -> Self {
298 Self::QueueRunComplete { summary }
299 }
300
301 #[must_use]
303 pub fn id(&self) -> Option<&str> {
304 match self {
305 Self::QueueSnapshot { .. } | Self::QueueRunComplete { .. } => None,
306 Self::DownloadStarted { id, .. }
307 | Self::DownloadProgress { id, .. }
308 | Self::ShardProgress { id, .. }
309 | Self::DownloadCompleted { id, .. }
310 | Self::DownloadFailed { id, .. }
311 | Self::DownloadCancelled { id } => Some(id),
312 }
313 }
314
315 #[must_use]
321 pub const fn event_name(&self) -> &'static str {
322 match self {
323 Self::QueueSnapshot { .. } => "download:queue_snapshot",
324 Self::DownloadStarted { .. } => "download:started",
325 Self::DownloadProgress { .. } | Self::ShardProgress { .. } => "download:progress",
326 Self::DownloadCompleted { .. } => "download:completed",
327 Self::DownloadFailed { .. } => "download:failed",
328 Self::DownloadCancelled { .. } => "download:cancelled",
329 Self::QueueRunComplete { .. } => "download:queue_run_complete",
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_progress_event_calculations() {
340 let event = DownloadEvent::progress("id", 500, 1000, 100.0);
341 match event {
342 DownloadEvent::DownloadProgress {
343 percentage,
344 eta_seconds,
345 ..
346 } => {
347 assert!((percentage - 50.0).abs() < 0.01);
348 assert!((eta_seconds - 5.0).abs() < 0.01);
349 }
350 _ => panic!("Expected DownloadProgress"),
351 }
352 }
353
354 #[test]
355 fn test_event_id_extraction() {
356 assert_eq!(DownloadEvent::started("test").id(), Some("test"));
357 assert_eq!(DownloadEvent::cancelled("test").id(), Some("test"));
358 assert!(DownloadEvent::queue_snapshot(vec![], 10).id().is_none());
359 }
360}