Skip to main content

jellytau_lib/download/
events.rs

1//! Download events for progress tracking and status updates
2
3use serde::{Deserialize, Serialize};
4
5/// Events emitted during download operations
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(tag = "type", rename_all = "camelCase")]
8pub enum DownloadEvent {
9    /// Download has been queued
10    #[serde(rename_all = "camelCase")]
11    Queued { download_id: i64, item_id: String },
12    /// Download has started
13    #[serde(rename_all = "camelCase")]
14    Started { download_id: i64, item_id: String },
15    /// Download progress update
16    #[serde(rename_all = "camelCase")]
17    Progress {
18        download_id: i64,
19        item_id: String,
20        bytes_downloaded: i64,
21        total_bytes: Option<i64>,
22        progress: f64, // 0.0 to 1.0
23        /// `total_bytes` is a prediction rather than the server's
24        /// `Content-Length`, so `progress` stops short of 1.0 until the
25        /// download completes. TRACES: UR-071 | DR-290
26        estimated: bool,
27    },
28    /// Download completed successfully
29    #[serde(rename_all = "camelCase")]
30    Completed {
31        download_id: i64,
32        item_id: String,
33        file_path: String,
34        /// Bytes actually written. The frontend persists completion too, and
35        /// without this it fell back to the row's `file_size` — which is a
36        /// prediction for a transcode (DR-290), not the real size.
37        bytes_downloaded: i64,
38    },
39    /// Download failed with error
40    #[serde(rename_all = "camelCase")]
41    Failed {
42        download_id: i64,
43        item_id: String,
44        error: String,
45    },
46    /// Download paused
47    #[serde(rename_all = "camelCase")]
48    Paused { download_id: i64, item_id: String },
49    /// Download cancelled
50    #[serde(rename_all = "camelCase")]
51    Cancelled { download_id: i64, item_id: String },
52    /// The queue is holding: WiFi-only is enabled and the current network is
53    /// metered/cellular. Pending rows stay pending and resume on network change.
54    ///
55    /// TRACES: UR-053 | DR-074
56    WaitingForNetwork,
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_download_event_serialization_roundtrip() {
65        let event = DownloadEvent::Progress {
66            download_id: 1,
67            item_id: "test123".to_string(),
68            bytes_downloaded: 1024,
69            total_bytes: Some(2048),
70            progress: 0.5,
71            estimated: false,
72        };
73
74        let json = serde_json::to_string(&event).unwrap();
75        let deserialized: DownloadEvent = serde_json::from_str(&json).unwrap();
76
77        match deserialized {
78            DownloadEvent::Progress {
79                download_id,
80                item_id,
81                progress,
82                ..
83            } => {
84                assert_eq!(download_id, 1);
85                assert_eq!(item_id, "test123");
86                assert_eq!(progress, 0.5);
87            }
88            _ => panic!("Wrong variant"),
89        }
90    }
91
92    #[test]
93    fn test_download_event_completed() {
94        let event = DownloadEvent::Completed {
95            download_id: 42,
96            item_id: "song456".to_string(),
97            file_path: "/path/to/file.mp3".to_string(),
98            bytes_downloaded: 4096,
99        };
100
101        let json = serde_json::to_string(&event).unwrap();
102        assert!(json.contains("\"type\":\"completed\""));
103        // Verify camelCase field names
104        assert!(
105            json.contains("\"downloadId\":42"),
106            "Expected downloadId (camelCase), got: {}",
107            json
108        );
109        assert!(
110            json.contains("\"itemId\":\"song456\""),
111            "Expected itemId (camelCase), got: {}",
112            json
113        );
114        assert!(
115            json.contains("\"filePath\":"),
116            "Expected filePath (camelCase), got: {}",
117            json
118        );
119
120        // Verify roundtrip
121        let deserialized: DownloadEvent = serde_json::from_str(&json).unwrap();
122        match deserialized {
123            DownloadEvent::Completed { file_path, .. } => {
124                assert_eq!(file_path, "/path/to/file.mp3");
125            }
126            _ => panic!("Wrong variant"),
127        }
128    }
129
130    #[test]
131    fn test_download_event_failed() {
132        let event = DownloadEvent::Failed {
133            download_id: 10,
134            item_id: "failed_item".to_string(),
135            error: "Network timeout".to_string(),
136        };
137
138        let json = serde_json::to_string(&event).unwrap();
139        assert!(json.contains("\"type\":\"failed\""));
140
141        // Verify roundtrip
142        let deserialized: DownloadEvent = serde_json::from_str(&json).unwrap();
143        match deserialized {
144            DownloadEvent::Failed { error, .. } => {
145                assert_eq!(error, "Network timeout");
146            }
147            _ => panic!("Wrong variant"),
148        }
149    }
150}