1pub mod cache;
10pub mod events;
11pub mod network;
12pub mod stop;
13pub mod worker;
14
15use crate::utils::lock::MutexSafe;
16use std::collections::HashSet;
17use std::path::PathBuf;
18use std::sync::{Arc, Mutex};
19
20pub use worker::DownloadWorker;
21
22pub struct DownloadManager {
24 max_concurrent: usize,
26 active_downloads: Arc<Mutex<HashSet<i64>>>,
28}
29
30impl DownloadManager {
31 pub fn new(_media_dir: PathBuf) -> Self {
33 Self {
34 max_concurrent: 3,
35 active_downloads: Arc::new(Mutex::new(HashSet::new())),
36 }
37 }
38
39 pub fn max_concurrent(&self) -> usize {
41 self.max_concurrent
42 }
43
44 pub fn set_max_concurrent(&mut self, max: usize) {
46 self.max_concurrent = max.max(1); }
48
49 pub fn can_start_download(&self) -> bool {
51 let active = self.active_downloads.lock_safe();
52 active.len() < self.max_concurrent
53 }
54
55 pub fn active_count(&self) -> usize {
57 self.active_downloads.lock_safe().len()
58 }
59
60 pub fn register_download(&self, download_id: i64) -> bool {
62 let mut active = self.active_downloads.lock_safe();
63 if active.len() >= self.max_concurrent {
64 return false;
65 }
66 active.insert(download_id)
67 }
68
69 pub fn unregister_download(&self, download_id: i64) {
71 let mut active = self.active_downloads.lock_safe();
72 active.remove(&download_id);
73 }
74
75 pub fn get_active_downloads(&self) -> Arc<Mutex<HashSet<i64>>> {
77 self.active_downloads.clone()
78 }
79}
80
81#[derive(specta::Type, Debug, Clone, serde::Serialize, serde::Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct DownloadInfo {
85 pub id: i64,
86 pub item_id: String,
87 pub user_id: String,
88 pub file_path: String,
89 pub file_size: Option<i64>,
90 pub mime_type: Option<String>,
91 pub status: String,
92 pub progress: f64,
93 pub bytes_downloaded: i64,
94 pub queued_at: String,
95 pub started_at: Option<String>,
96 pub completed_at: Option<String>,
97 pub error_message: Option<String>,
98 pub retry_count: i32,
99 pub priority: i32,
100 pub item_name: Option<String>,
102 pub artist_name: Option<String>,
103 pub album_name: Option<String>,
104 pub series_name: Option<String>,
106 pub season_name: Option<String>,
107 pub episode_number: Option<i32>,
108 pub season_number: Option<i32>,
109 pub quality_preset: Option<String>,
110 pub media_type: String,
111 pub download_source: String, }
114
115#[derive(Debug, Clone)]
117pub struct DownloadTask {
118 pub url: String,
119 pub target_path: PathBuf,
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_download_manager_set_max_concurrent() {
128 let media_dir = PathBuf::from("/tmp/jellytau/media");
129 let mut manager = DownloadManager::new(media_dir);
130
131 manager.set_max_concurrent(5);
132 assert_eq!(manager.max_concurrent(), 5);
133
134 manager.set_max_concurrent(0);
136 assert_eq!(manager.max_concurrent(), 1);
137 }
138
139 #[test]
140 fn test_download_info_serialization() {
141 let info = DownloadInfo {
142 id: 1,
143 item_id: "test123".to_string(),
144 user_id: "user1".to_string(),
145 file_path: "/path/to/file.mp3".to_string(),
146 file_size: Some(1024000),
147 mime_type: Some("audio/mpeg".to_string()),
148 status: "downloading".to_string(),
149 progress: 0.5,
150 bytes_downloaded: 512000,
151 queued_at: "2024-01-01T00:00:00Z".to_string(),
152 started_at: Some("2024-01-01T00:01:00Z".to_string()),
153 completed_at: None,
154 error_message: None,
155 retry_count: 0,
156 priority: 0,
157 item_name: Some("Test Song".to_string()),
158 artist_name: Some("Test Artist".to_string()),
159 album_name: Some("Test Album".to_string()),
160 series_name: None,
161 season_name: None,
162 episode_number: None,
163 season_number: None,
164 quality_preset: Some("original".to_string()),
165 media_type: "audio".to_string(),
166 download_source: "user".to_string(),
167 };
168
169 let json = serde_json::to_string(&info).unwrap();
170 assert!(json.contains("\"status\":\"downloading\""));
171 assert!(json.contains("\"progress\":0.5"));
172 assert!(json.contains("\"itemName\":\"Test Song\""));
173 assert!(json.contains("\"mediaType\":\"audio\""));
174 }
175
176 #[test]
177 fn test_video_download_info_serialization() {
178 let info = DownloadInfo {
179 id: 2,
180 item_id: "episode123".to_string(),
181 user_id: "user1".to_string(),
182 file_path: "/path/to/ShowName/S01E01_Title.mp4".to_string(),
183 file_size: Some(1024000000),
184 mime_type: Some("video/mp4".to_string()),
185 status: "completed".to_string(),
186 progress: 1.0,
187 bytes_downloaded: 1024000000,
188 queued_at: "2024-01-01T00:00:00Z".to_string(),
189 started_at: Some("2024-01-01T00:01:00Z".to_string()),
190 completed_at: Some("2024-01-01T01:00:00Z".to_string()),
191 error_message: None,
192 retry_count: 0,
193 priority: 100,
194 item_name: Some("Episode Title".to_string()),
195 artist_name: None,
196 album_name: None,
197 series_name: Some("Show Name".to_string()),
198 season_name: Some("Season 1".to_string()),
199 episode_number: Some(1),
200 season_number: Some(1),
201 quality_preset: Some("high".to_string()),
202 media_type: "video".to_string(),
203 download_source: "auto".to_string(),
204 };
205
206 let json = serde_json::to_string(&info).unwrap();
207 assert!(json.contains("\"mediaType\":\"video\""));
208 assert!(json.contains("\"seriesName\":\"Show Name\""));
209 assert!(json.contains("\"episodeNumber\":1"));
210 assert!(json.contains("\"qualityPreset\":\"high\""));
211 }
212}