usenet_dl/lib.rs
1//! # usenet-dl
2//!
3//! Highly configurable backend library for Usenet download applications.
4//!
5//! ## Design Philosophy
6//!
7//! usenet-dl is designed to be:
8//! - **Highly configurable** - Almost every behavior can be customized
9//! - **Sensible defaults** - Works out of the box with zero configuration
10//! - **Library-first** - No CLI or UI, purely a Rust crate for embedding
11//! - **Event-driven** - Consumers subscribe to events, no polling required
12//!
13//! ## Quick Start
14//!
15//! ```no_run
16//! use usenet_dl::{UsenetDownloader, Config, ServerConfig};
17//!
18//! #[tokio::main]
19//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
20//! let config = Config {
21//! servers: vec![
22//! ServerConfig {
23//! host: "news.example.com".to_string(),
24//! port: 563,
25//! tls: true,
26//! username: Some("user".to_string()),
27//! password: Some("pass".to_string()),
28//! connections: 10,
29//! priority: 0,
30//! pipeline_depth: 10,
31//! }
32//! ],
33//! ..Default::default()
34//! };
35//!
36//! let downloader = UsenetDownloader::new(config).await?;
37//!
38//! // Subscribe to events
39//! let mut events = downloader.subscribe();
40//! tokio::spawn(async move {
41//! while let Ok(event) = events.recv().await {
42//! println!("Event: {:?}", event);
43//! }
44//! });
45//!
46//! Ok(())
47//! }
48//! ```
49
50#![warn(missing_docs)]
51#![warn(clippy::all)]
52#![warn(clippy::unwrap_used)]
53#![warn(clippy::expect_used)]
54
55/// REST API module
56pub mod api;
57/// Configuration types
58pub mod config;
59/// Database persistence layer
60pub mod db;
61/// Filename deobfuscation
62pub mod deobfuscation;
63/// Core downloader implementation (decomposed into focused submodules)
64pub mod downloader;
65/// Error types
66pub mod error;
67/// Archive extraction
68pub mod extraction;
69/// Folder watching for automatic NZB import
70pub mod folder_watcher;
71/// PAR2 parity handling
72pub mod parity;
73/// Post-processing pipeline
74pub mod post_processing;
75/// Retry logic with exponential backoff
76pub mod retry;
77/// RSS feed management
78pub mod rss_manager;
79/// RSS feed scheduler
80pub mod rss_scheduler;
81/// Time-based scheduling
82pub mod scheduler;
83/// Scheduler task execution
84pub mod scheduler_task;
85/// Speed limiting with token bucket
86pub mod speed_limiter;
87/// Core types and events
88pub mod types;
89/// Utility functions
90pub mod utils;
91
92// Re-export commonly used types
93pub use config::{Config, DuplicateAction, ServerConfig};
94pub use db::Database;
95pub use downloader::UsenetDownloader;
96pub use error::{
97 ApiError, DatabaseError, DownloadError, Error, ErrorDetail, PostProcessError, Result,
98 ToHttpStatus,
99};
100pub use parity::{
101 CliParityHandler, NoOpParityHandler, ParityCapabilities, ParityHandler, RepairResult,
102 VerifyResult,
103};
104pub use scheduler::{RuleId, ScheduleAction, ScheduleRule, Scheduler, Weekday};
105pub use types::{
106 DownloadId, DownloadInfo, DownloadOptions, DuplicateInfo, Event, HistoryEntry, Priority,
107 QueueStats, ServerCapabilities, ServerTestResult, Stage, Status,
108};
109
110/// Helper function to run the downloader with graceful signal handling.
111///
112/// Waits for a termination signal and then calls the downloader's `shutdown()` method.
113///
114/// - **Unix:** listens for SIGTERM and SIGINT, with fallbacks if signal registration fails.
115/// - **Windows/other:** listens for Ctrl+C via `tokio::signal::ctrl_c()`.
116///
117/// # Example
118///
119/// ```no_run
120/// use usenet_dl::{UsenetDownloader, Config, run_with_shutdown};
121///
122/// #[tokio::main]
123/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
124/// let config = Config::default();
125/// let downloader = UsenetDownloader::new(config).await?;
126///
127/// // Run with automatic signal handling
128/// run_with_shutdown(downloader).await?;
129///
130/// Ok(())
131/// }
132/// ```
133pub async fn run_with_shutdown(downloader: UsenetDownloader) -> Result<()> {
134 wait_for_signal().await;
135 downloader.shutdown().await
136}
137
138#[cfg(unix)]
139async fn wait_for_signal() {
140 use tokio::signal::unix::{SignalKind, signal};
141
142 // Set up signal handlers - these may fail in restricted environments (containers, tests)
143 let sigterm_result = signal(SignalKind::terminate());
144 let sigint_result = signal(SignalKind::interrupt());
145
146 match (sigterm_result, sigint_result) {
147 (Ok(mut sigterm), Ok(mut sigint)) => {
148 tokio::select! {
149 _ = sigterm.recv() => {
150 tracing::info!("Received SIGTERM signal");
151 }
152 _ = sigint.recv() => {
153 tracing::info!("Received SIGINT signal (Ctrl+C)");
154 }
155 }
156 }
157 (Err(e), _) => {
158 tracing::warn!(error = %e, "Could not register SIGTERM handler, waiting for SIGINT only");
159 if let Ok(mut sigint) = signal(SignalKind::interrupt()) {
160 sigint.recv().await;
161 tracing::info!("Received SIGINT signal (Ctrl+C)");
162 } else {
163 tracing::error!("Could not register any signal handlers, using ctrl_c fallback");
164 tokio::signal::ctrl_c().await.ok();
165 }
166 }
167 (_, Err(e)) => {
168 tracing::warn!(error = %e, "Could not register SIGINT handler, waiting for SIGTERM only");
169 if let Ok(mut sigterm) = signal(SignalKind::terminate()) {
170 sigterm.recv().await;
171 tracing::info!("Received SIGTERM signal");
172 } else {
173 tracing::error!("Could not register any signal handlers, using ctrl_c fallback");
174 tokio::signal::ctrl_c().await.ok();
175 }
176 }
177 }
178}
179
180#[cfg(not(unix))]
181async fn wait_for_signal() {
182 match tokio::signal::ctrl_c().await {
183 Ok(()) => {
184 tracing::info!("Received Ctrl+C signal");
185 }
186 Err(e) => {
187 tracing::error!(error = %e, "Failed to listen for Ctrl+C signal");
188 }
189 }
190}