1use std::path::PathBuf;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum JobStatus {
13 Queued,
14 Downloading,
15 Paused,
16 Verifying,
17 Repairing,
18 Extracting,
19 PostProcessing,
20 Completed,
21 Failed,
22}
23
24impl std::fmt::Display for JobStatus {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 Self::Queued => write!(f, "Queued"),
28 Self::Downloading => write!(f, "Downloading"),
29 Self::Paused => write!(f, "Paused"),
30 Self::Verifying => write!(f, "Verifying"),
31 Self::Repairing => write!(f, "Repairing"),
32 Self::Extracting => write!(f, "Extracting"),
33 Self::PostProcessing => write!(f, "PostProcessing"),
34 Self::Completed => write!(f, "Completed"),
35 Self::Failed => write!(f, "Failed"),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum Priority {
47 Low = 0,
48 #[default]
49 Normal = 1,
50 High = 2,
51 Force = 3,
52}
53
54#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct ServerArticleStats {
61 pub server_id: String,
62 pub server_name: String,
63 pub articles_downloaded: usize,
64 pub articles_failed: usize,
65 pub bytes_downloaded: u64,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct NzbJob {
71 pub id: String,
73 pub name: String,
75 pub category: String,
77 pub status: JobStatus,
79 pub priority: Priority,
81 pub total_bytes: u64,
83 pub downloaded_bytes: u64,
85 pub file_count: usize,
87 pub files_completed: usize,
89 pub article_count: usize,
91 pub articles_downloaded: usize,
93 pub articles_failed: usize,
95 pub added_at: DateTime<Utc>,
97 pub completed_at: Option<DateTime<Utc>>,
99 pub work_dir: PathBuf,
101 pub output_dir: PathBuf,
103 pub password: Option<String>,
105 pub error_message: Option<String>,
107 #[serde(default)]
109 pub speed_bps: u64,
110 #[serde(default)]
112 pub server_stats: Vec<ServerArticleStats>,
113 #[serde(skip)]
115 pub files: Vec<NzbFile>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct NzbFile {
121 pub id: String,
123 pub filename: String,
125 pub bytes: u64,
127 pub bytes_downloaded: u64,
129 pub is_par2: bool,
131 pub par2_setname: Option<String>,
133 pub par2_vol: Option<u32>,
135 pub par2_blocks: Option<u32>,
137 pub assembled: bool,
139 pub groups: Vec<String>,
141 #[serde(skip)]
143 pub articles: Vec<Article>,
144}
145
146pub use nzb_nntp::Article;
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct HistoryEntry {
156 pub id: String,
157 pub name: String,
158 pub category: String,
159 pub status: JobStatus,
160 pub total_bytes: u64,
161 pub downloaded_bytes: u64,
162 pub added_at: DateTime<Utc>,
163 pub completed_at: DateTime<Utc>,
164 pub output_dir: PathBuf,
165 pub stages: Vec<StageResult>,
167 pub error_message: Option<String>,
168 #[serde(default)]
170 pub server_stats: Vec<ServerArticleStats>,
171 #[serde(skip_serializing)]
173 pub nzb_data: Option<Vec<u8>>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct StageResult {
178 pub name: String,
179 pub status: StageStatus,
180 pub message: Option<String>,
181 pub duration_secs: f64,
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(rename_all = "snake_case")]
186pub enum StageStatus {
187 Success,
188 Failed,
189 Skipped,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct RssItem {
199 pub id: String,
201 pub feed_name: String,
203 pub title: String,
205 pub url: Option<String>,
207 pub published_at: Option<DateTime<Utc>>,
209 pub first_seen_at: DateTime<Utc>,
211 pub downloaded: bool,
213 pub downloaded_at: Option<DateTime<Utc>>,
215 pub category: Option<String>,
217 pub size_bytes: u64,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct RssRule {
224 pub id: String,
226 pub name: String,
228 pub feed_names: Vec<String>,
230 pub category: Option<String>,
232 pub priority: i32,
234 pub match_regex: String,
236 pub enabled: bool,
238}
239
240#[cfg(feature = "groups-db")]
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct GroupRow {
247 pub id: i64,
248 pub name: String,
249 pub description: Option<String>,
250 pub subscribed: bool,
251 pub article_count: i64,
252 pub first_article: i64,
253 pub last_article: i64,
254 pub last_scanned: i64,
255 pub last_updated: Option<String>,
256 pub created_at: String,
257 #[serde(default)]
258 pub unread_count: i64,
259}
260
261#[cfg(feature = "groups-db")]
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct HeaderRow {
264 pub id: i64,
265 pub group_id: i64,
266 pub article_num: i64,
267 pub subject: String,
268 pub author: String,
269 pub date: String,
270 pub message_id: String,
271 pub references_: String,
272 pub bytes: i64,
273 pub lines: i64,
274 pub read: bool,
275 pub downloaded_at: String,
276}
277
278#[cfg(feature = "groups-db")]
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct ThreadSummary {
281 pub root_message_id: String,
282 pub subject: String,
283 pub author: String,
284 pub date: String,
285 pub last_reply_date: String,
286 pub reply_count: i64,
287 pub unread_count: i64,
288}
289
290#[cfg(feature = "groups-db")]
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ThreadArticle {
293 #[serde(flatten)]
294 pub header: HeaderRow,
295 pub depth: i32,
296}
297
298#[cfg(feature = "groups-db")]
299#[derive(Debug, Clone, Deserialize)]
300pub struct MarkReadInput {
301 pub header_ids: Vec<i64>,
302}
303
304#[cfg(feature = "groups-db")]
305#[derive(Debug, Clone, Deserialize)]
306pub struct DownloadSelectedInput {
307 pub message_ids: Vec<String>,
308 pub name: Option<String>,
309 pub category: Option<String>,
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn test_job_status_display() {
318 assert_eq!(JobStatus::Queued.to_string(), "Queued");
319 assert_eq!(JobStatus::Downloading.to_string(), "Downloading");
320 assert_eq!(JobStatus::Paused.to_string(), "Paused");
321 assert_eq!(JobStatus::Verifying.to_string(), "Verifying");
322 assert_eq!(JobStatus::Repairing.to_string(), "Repairing");
323 assert_eq!(JobStatus::Extracting.to_string(), "Extracting");
324 assert_eq!(JobStatus::PostProcessing.to_string(), "PostProcessing");
325 assert_eq!(JobStatus::Completed.to_string(), "Completed");
326 assert_eq!(JobStatus::Failed.to_string(), "Failed");
327 }
328
329 #[test]
330 fn test_job_status_serde_roundtrip() {
331 let statuses = [
332 JobStatus::Queued,
333 JobStatus::Downloading,
334 JobStatus::Paused,
335 JobStatus::Completed,
336 JobStatus::Failed,
337 ];
338
339 for status in &statuses {
340 let json = serde_json::to_string(status).unwrap();
341 let restored: JobStatus = serde_json::from_str(&json).unwrap();
342 assert_eq!(*status, restored);
343 }
344 }
345
346 #[test]
347 fn test_job_status_serde_snake_case() {
348 let json = serde_json::to_string(&JobStatus::PostProcessing).unwrap();
349 assert_eq!(json, "\"post_processing\"");
350
351 let restored: JobStatus = serde_json::from_str("\"post_processing\"").unwrap();
352 assert_eq!(restored, JobStatus::PostProcessing);
353 }
354
355 #[test]
356 fn test_priority_default() {
357 let p = Priority::default();
358 assert_eq!(p, Priority::Normal);
359 }
360
361 #[test]
362 fn test_priority_ordering() {
363 assert!(Priority::Low < Priority::Normal);
364 assert!(Priority::Normal < Priority::High);
365 assert!(Priority::High < Priority::Force);
366 }
367
368 #[test]
369 fn test_priority_values() {
370 assert_eq!(Priority::Low as i32, 0);
371 assert_eq!(Priority::Normal as i32, 1);
372 assert_eq!(Priority::High as i32, 2);
373 assert_eq!(Priority::Force as i32, 3);
374 }
375
376 #[test]
377 fn test_priority_serde_roundtrip() {
378 for p in [
379 Priority::Low,
380 Priority::Normal,
381 Priority::High,
382 Priority::Force,
383 ] {
384 let json = serde_json::to_string(&p).unwrap();
385 let restored: Priority = serde_json::from_str(&json).unwrap();
386 assert_eq!(p, restored);
387 }
388 }
389
390 #[test]
391 fn test_stage_status_serde() {
392 let statuses = [
393 StageStatus::Success,
394 StageStatus::Failed,
395 StageStatus::Skipped,
396 ];
397 for s in &statuses {
398 let json = serde_json::to_string(s).unwrap();
399 let restored: StageStatus = serde_json::from_str(&json).unwrap();
400 assert_eq!(*s, restored);
401 }
402 }
403
404 #[test]
405 fn test_stage_status_snake_case() {
406 assert_eq!(
407 serde_json::to_string(&StageStatus::Success).unwrap(),
408 "\"success\""
409 );
410 assert_eq!(
411 serde_json::to_string(&StageStatus::Failed).unwrap(),
412 "\"failed\""
413 );
414 assert_eq!(
415 serde_json::to_string(&StageStatus::Skipped).unwrap(),
416 "\"skipped\""
417 );
418 }
419
420 #[test]
421 fn test_stage_result_serde() {
422 let sr = StageResult {
423 name: "Verify".into(),
424 status: StageStatus::Success,
425 message: Some("OK".into()),
426 duration_secs: 2.5,
427 };
428 let json = serde_json::to_string(&sr).unwrap();
429 let restored: StageResult = serde_json::from_str(&json).unwrap();
430 assert_eq!(restored.name, "Verify");
431 assert_eq!(restored.status, StageStatus::Success);
432 assert_eq!(restored.message.as_deref(), Some("OK"));
433 assert!((restored.duration_secs - 2.5).abs() < 0.001);
434 }
435
436 #[test]
437 fn test_server_article_stats_default() {
438 let stats = ServerArticleStats::default();
439 assert!(stats.server_id.is_empty());
440 assert_eq!(stats.articles_downloaded, 0);
441 assert_eq!(stats.articles_failed, 0);
442 assert_eq!(stats.bytes_downloaded, 0);
443 }
444
445 #[test]
446 fn test_server_article_stats_serde() {
447 let stats = ServerArticleStats {
448 server_id: "srv-1".into(),
449 server_name: "Provider".into(),
450 articles_downloaded: 100,
451 articles_failed: 5,
452 bytes_downloaded: 75_000_000,
453 };
454 let json = serde_json::to_string(&stats).unwrap();
455 let restored: ServerArticleStats = serde_json::from_str(&json).unwrap();
456 assert_eq!(restored.server_id, "srv-1");
457 assert_eq!(restored.articles_downloaded, 100);
458 assert_eq!(restored.articles_failed, 5);
459 assert_eq!(restored.bytes_downloaded, 75_000_000);
460 }
461}