Skip to main content

usenet_dl/db/
mod.rs

1//! Database layer for usenet-dl
2//!
3//! Handles SQLite persistence for downloads, articles, passwords, and history.
4//!
5//! ## Submodules
6//!
7//! Methods on [`Database`] are organized by domain:
8//! - [`migrations`] — Database lifecycle, schema migrations
9//! - [`downloads`] — Download queue CRUD
10//! - [`articles`] — Article-level tracking for resume support
11//! - [`passwords`] — Password cache for archive extraction
12//! - [`duplicates`] — Duplicate detection queries
13//! - [`history`] — History management
14//! - [`state`] — Runtime state (shutdown tracking, NZB processing, RSS seen)
15//! - [`rss`] — RSS feed CRUD
16
17use crate::types::{HistoryEntry, Status};
18use sqlx::{FromRow, sqlite::SqlitePool};
19use std::path::PathBuf;
20
21mod articles;
22mod downloads;
23mod duplicates;
24mod history;
25mod migrations;
26mod passwords;
27mod rss;
28mod state;
29
30/// New download to be inserted into the database
31#[derive(Debug, Clone)]
32pub struct NewDownload {
33    /// Display name for this download
34    pub name: String,
35    /// Path to the NZB file
36    pub nzb_path: String,
37    /// Original name from NZB metadata
38    pub nzb_meta_name: Option<String>,
39    /// SHA-256 hash of the NZB file for duplicate detection
40    pub nzb_hash: Option<String>,
41    /// Job name for post-processing scripts
42    pub job_name: Option<String>,
43    /// Category for organizing downloads
44    pub category: Option<String>,
45    /// Destination directory for extracted files
46    pub destination: String,
47    /// Post-processing flags (bitfield: 1=unpack, 2=verify, 4=repair, 8=delete)
48    pub post_process: i32,
49    /// Download priority (higher values download first)
50    pub priority: i32,
51    /// Current status (0=queued, 1=downloading, 2=completed, etc.)
52    pub status: i32,
53    /// Total size in bytes
54    pub size_bytes: i64,
55}
56
57/// Download record from database
58#[derive(Debug, Clone, FromRow)]
59pub struct Download {
60    /// Unique database ID
61    pub id: i64,
62    /// Display name for this download
63    pub name: String,
64    /// Path to the NZB file
65    pub nzb_path: String,
66    /// Original name from NZB metadata
67    pub nzb_meta_name: Option<String>,
68    /// SHA-256 hash of the NZB file for duplicate detection
69    pub nzb_hash: Option<String>,
70    /// Job name for post-processing scripts
71    pub job_name: Option<String>,
72    /// Category for organizing downloads
73    pub category: Option<String>,
74    /// Destination directory for extracted files
75    pub destination: String,
76    /// Post-processing flags (bitfield: 1=unpack, 2=verify, 4=repair, 8=delete)
77    pub post_process: i32,
78    /// Download priority (higher values download first)
79    pub priority: i32,
80    /// Current status (0=queued, 1=downloading, 2=completed, etc.)
81    pub status: i32,
82    /// Download progress as a fraction (0.0-1.0)
83    pub progress: f32,
84    /// Current download speed in bytes per second
85    pub speed_bps: i64,
86    /// Total size in bytes
87    pub size_bytes: i64,
88    /// Number of bytes downloaded so far
89    pub downloaded_bytes: i64,
90    /// Error message if download failed
91    pub error_message: Option<String>,
92    /// Unix timestamp when download was created
93    pub created_at: i64,
94    /// Unix timestamp when download started
95    pub started_at: Option<i64>,
96    /// Unix timestamp when download completed
97    pub completed_at: Option<i64>,
98    /// DirectUnpack state (0=NotStarted, 1=Active, 2=Completed, 3=Cancelled, 4=Failed)
99    pub direct_unpack_state: i32,
100    /// Number of files extracted by DirectUnpack (0 means vacuous completion)
101    pub direct_unpack_extracted_count: i32,
102}
103
104/// New article to be inserted into the database
105#[derive(Debug, Clone)]
106pub struct NewArticle {
107    /// Download this article belongs to
108    pub download_id: crate::types::DownloadId,
109    /// Usenet message-ID
110    pub message_id: String,
111    /// Segment number within the file
112    pub segment_number: i32,
113    /// Index of the NZB file this article belongs to (0-based)
114    pub file_index: i32,
115    /// Size of this segment in bytes
116    pub size_bytes: i64,
117}
118
119/// Article record from database
120#[derive(Debug, Clone, FromRow)]
121pub struct Article {
122    /// Unique database ID
123    pub id: i64,
124    /// Download this article belongs to
125    pub download_id: i64,
126    /// Usenet message-ID
127    pub message_id: String,
128    /// Segment number within the file
129    pub segment_number: i32,
130    /// Index of the NZB file this article belongs to (0-based)
131    pub file_index: i32,
132    /// Size of this segment in bytes
133    pub size_bytes: i64,
134    /// Article status (see [`article_status`])
135    pub status: i32,
136    /// Unix timestamp when article was downloaded
137    pub downloaded_at: Option<i64>,
138}
139
140/// New download file to be inserted into the database
141#[derive(Debug, Clone)]
142pub struct NewDownloadFile {
143    /// Download this file belongs to
144    pub download_id: crate::types::DownloadId,
145    /// Index of this file within the NZB (0-based)
146    pub file_index: i32,
147    /// Parsed filename (from NZB subject)
148    pub filename: String,
149    /// Original NZB subject line
150    pub subject: Option<String>,
151    /// Total number of segments in this file
152    pub total_segments: i32,
153}
154
155/// Download file record from database
156#[derive(Debug, Clone, FromRow)]
157pub struct DownloadFile {
158    /// Unique database ID
159    pub id: i64,
160    /// Download this file belongs to
161    pub download_id: i64,
162    /// Index of this file within the NZB (0-based)
163    pub file_index: i32,
164    /// Parsed filename (from NZB subject)
165    pub filename: String,
166    /// Original NZB subject line
167    pub subject: Option<String>,
168    /// Total number of segments in this file
169    pub total_segments: i32,
170    /// Whether this file is paused (0=no, 1=yes)
171    pub paused: i32,
172    /// Whether all segments of this file have been downloaded (0=no, 1=yes)
173    pub completed: i32,
174    /// Original filename before DirectRename (None if not renamed)
175    pub original_filename: Option<String>,
176}
177
178/// Article status constants
179pub mod article_status {
180    /// Article is queued and not yet downloaded
181    pub const PENDING: i32 = 0;
182    /// Article has been successfully downloaded
183    pub const DOWNLOADED: i32 = 1;
184    /// Article download failed
185    pub const FAILED: i32 = 2;
186}
187
188/// New history entry to be inserted into the database
189#[derive(Debug, Clone)]
190pub struct NewHistoryEntry {
191    /// Download name
192    pub name: String,
193    /// Category label
194    pub category: Option<String>,
195    /// Destination directory on disk
196    pub destination: Option<PathBuf>,
197    /// Completion status code
198    pub status: i32,
199    /// Total size in bytes
200    pub size_bytes: u64,
201    /// Total download duration in seconds
202    pub download_time_secs: i64,
203    /// Unix timestamp when download completed
204    pub completed_at: i64,
205}
206
207/// History record from database (raw from SQLite)
208#[derive(Debug, Clone, FromRow)]
209pub struct HistoryRow {
210    /// Unique database ID
211    pub id: i64,
212    /// Download name
213    pub name: String,
214    /// Category label
215    pub category: Option<String>,
216    /// Destination directory on disk
217    pub destination: Option<String>,
218    /// Completion status code
219    pub status: i32,
220    /// Total size in bytes
221    pub size_bytes: i64,
222    /// Total download duration in seconds
223    pub download_time_secs: i64,
224    /// Unix timestamp when download completed
225    pub completed_at: i64,
226}
227
228impl From<HistoryRow> for HistoryEntry {
229    fn from(row: HistoryRow) -> Self {
230        use chrono::{TimeZone, Utc};
231        use std::time::Duration;
232
233        HistoryEntry {
234            id: row.id,
235            name: row.name,
236            category: row.category,
237            destination: row.destination.map(PathBuf::from),
238            status: Status::from_i32(row.status),
239            size_bytes: row.size_bytes as u64,
240            download_time: Duration::from_secs(row.download_time_secs as u64),
241            completed_at: Utc
242                .timestamp_opt(row.completed_at, 0)
243                .single()
244                .unwrap_or_else(Utc::now),
245        }
246    }
247}
248
249/// RSS feed record from database
250#[derive(Debug, Clone, FromRow)]
251pub struct RssFeed {
252    /// Unique database ID
253    pub id: i64,
254    /// Feed display name
255    pub name: String,
256    /// Feed URL
257    pub url: String,
258    /// Interval between checks in seconds
259    pub check_interval_secs: i64,
260    /// Category label for matched downloads
261    pub category: Option<String>,
262    /// Whether to automatically download matched items (0 = no, 1 = yes)
263    pub auto_download: i32,
264    /// Download priority
265    pub priority: i32,
266    /// Whether the feed is enabled (0 = disabled, 1 = enabled)
267    pub enabled: i32,
268    /// Unix timestamp of last feed check
269    pub last_check: Option<i64>,
270    /// Last error message from checking the feed
271    pub last_error: Option<String>,
272    /// Unix timestamp when feed was created
273    pub created_at: i64,
274}
275
276/// RSS filter record from database
277#[derive(Debug, Clone, FromRow)]
278pub struct RssFilterRow {
279    /// Unique database ID
280    pub id: i64,
281    /// ID of the parent RSS feed
282    pub feed_id: i64,
283    /// Filter display name
284    pub name: String,
285    /// Comma-separated include patterns
286    pub include_patterns: Option<String>,
287    /// Comma-separated exclude patterns
288    pub exclude_patterns: Option<String>,
289    /// Minimum file size in bytes
290    pub min_size: Option<i64>,
291    /// Maximum file size in bytes
292    pub max_size: Option<i64>,
293    /// Maximum age of items in seconds
294    pub max_age_secs: Option<i64>,
295}
296
297/// Parameters for inserting a new RSS feed
298pub struct InsertRssFeedParams<'a> {
299    /// Feed name
300    pub name: &'a str,
301    /// Feed URL
302    pub url: &'a str,
303    /// Check interval in seconds
304    pub check_interval_secs: i64,
305    /// Optional category
306    pub category: Option<&'a str>,
307    /// Whether to auto-download matching items
308    pub auto_download: bool,
309    /// Download priority
310    pub priority: i32,
311    /// Whether the feed is enabled
312    pub enabled: bool,
313}
314
315/// Parameters for updating an existing RSS feed
316pub struct UpdateRssFeedParams<'a> {
317    /// Feed ID
318    pub id: i64,
319    /// Feed name
320    pub name: &'a str,
321    /// Feed URL
322    pub url: &'a str,
323    /// Check interval in seconds
324    pub check_interval_secs: i64,
325    /// Optional category
326    pub category: Option<&'a str>,
327    /// Whether to auto-download matching items
328    pub auto_download: bool,
329    /// Download priority
330    pub priority: i32,
331    /// Whether the feed is enabled
332    pub enabled: bool,
333}
334
335/// Parameters for inserting a new RSS filter
336pub struct InsertRssFilterParams<'a> {
337    /// Feed ID this filter belongs to
338    pub feed_id: i64,
339    /// Filter name
340    pub name: &'a str,
341    /// Include patterns (comma-separated regex)
342    pub include_patterns: Option<&'a str>,
343    /// Exclude patterns (comma-separated regex)
344    pub exclude_patterns: Option<&'a str>,
345    /// Minimum file size in bytes
346    pub min_size: Option<i64>,
347    /// Maximum file size in bytes
348    pub max_size: Option<i64>,
349    /// Maximum age in seconds
350    pub max_age_secs: Option<i64>,
351}
352
353/// Database handle for usenet-dl
354pub struct Database {
355    pool: SqlitePool,
356}
357
358// unwrap/expect are acceptable in tests for concise failure-on-error assertions
359#[allow(clippy::unwrap_used, clippy::expect_used)]
360#[cfg(test)]
361mod tests;