Skip to main content

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}