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