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;