kget/torrent/mod.rs
1//! BitTorrent download support.
2//!
3//! This module provides torrent downloading capabilities through multiple backends:
4//!
5//! - **Native** (`torrent-native` feature): Built-in BitTorrent client using librqbit
6//! - **Transmission** (`torrent-transmission` feature): Transmission daemon RPC
7//! - **External**: Opens magnet links in the system's default torrent client
8//!
9//! # Native Torrent Client
10//!
11//! The native client provides full BitTorrent protocol support:
12//! - Magnet link parsing and metadata download
13//! - DHT (Distributed Hash Table) for peer discovery
14//! - Parallel piece downloading
15//! - Progress callbacks for UI integration
16//!
17//! # Example
18//!
19//! ```rust,no_run
20//! use kget::torrent::{download_magnet, TorrentCallbacks};
21//! use kget::{ProxyConfig, Optimizer};
22//! use std::sync::Arc;
23//!
24//! let callbacks = TorrentCallbacks {
25//! status: Some(Arc::new(|msg| println!("{}", msg))),
26//! progress: Some(Arc::new(|p| println!("{:.1}%", p * 100.0))),
27//! };
28//!
29//! download_magnet(
30//! "magnet:?xt=urn:btih:...",
31//! "./downloads",
32//! false,
33//! ProxyConfig::default(),
34//! Optimizer::new(),
35//! callbacks,
36//! ).expect("Torrent download failed");
37//! ```
38//!
39//! # Backend Selection
40//!
41//! The backend is selected via the `KGET_TORRENT_BACKEND` environment variable:
42//! - `native`: Use built-in client (requires `torrent-native` feature)
43//! - `transmission`: Use Transmission RPC (requires `torrent-transmission` feature)
44//! - Any other value: Open in system's default torrent client
45//!
46//! If not set, defaults to `native` when available, otherwise `external`.
47
48use std::error::Error;
49use std::sync::Arc;
50
51use crate::config::ProxyConfig;
52use crate::optimization::Optimizer;
53
54mod external;
55mod settings;
56
57#[cfg(feature = "torrent-transmission")]
58mod transmission;
59
60#[cfg(feature = "torrent-native")]
61mod native;
62
63/// Type alias for status message callbacks.
64pub type StatusCb = Arc<dyn Fn(String) + Send + Sync>;
65
66/// Type alias for progress callbacks (0.0 to 1.0).
67pub type ProgressCb = Arc<dyn Fn(f32) + Send + Sync>;
68
69/// Callbacks for torrent download progress and status updates.
70///
71/// Both callbacks are optional. If not provided, no updates are sent.
72///
73/// # Example
74///
75/// ```rust
76/// use kget::torrent::TorrentCallbacks;
77/// use std::sync::Arc;
78///
79/// // With callbacks
80/// let callbacks = TorrentCallbacks {
81/// status: Some(Arc::new(|msg| println!("Status: {}", msg))),
82/// progress: Some(Arc::new(|p| println!("Progress: {:.1}%", p * 100.0))),
83/// };
84///
85/// // Without callbacks
86/// let silent = TorrentCallbacks::default();
87/// ```
88#[derive(Default, Clone)]
89pub struct TorrentCallbacks {
90 /// Callback for human-readable status messages
91 pub status: Option<StatusCb>,
92 /// Callback for progress updates (0.0 to 1.0)
93 pub progress: Option<ProgressCb>,
94}
95
96fn emit_status(cb: &TorrentCallbacks, msg: impl Into<String>) {
97 if let Some(f) = &cb.status {
98 f(msg.into());
99 }
100}
101
102fn emit_progress(cb: &TorrentCallbacks, p: f32) {
103 if let Some(f) = &cb.progress {
104 f(p.clamp(0.0, 1.0));
105 }
106}
107
108fn selected_backend() -> String {
109 std::env::var("KGET_TORRENT_BACKEND")
110 .unwrap_or_else(|_| {
111 // Default to native if available, otherwise external
112 #[cfg(feature = "torrent-native")]
113 { "native".to_string() }
114 #[cfg(not(feature = "torrent-native"))]
115 { "external".to_string() }
116 })
117 .to_lowercase()
118}
119
120/// Download a torrent from a magnet link.
121///
122/// This function automatically selects the best available backend:
123/// 1. Native client (if `torrent-native` feature is enabled)
124/// 2. Transmission RPC (if `torrent-transmission` feature is enabled)
125/// 3. External client (opens in system default torrent app)
126///
127/// Override the backend with `KGET_TORRENT_BACKEND` environment variable.
128///
129/// # Arguments
130///
131/// * `magnet` - Magnet link starting with `magnet:?`
132/// * `output_dir` - Directory to save downloaded files
133/// * `quiet` - Suppress console output
134/// * `proxy` - Proxy configuration (native backend only)
135/// * `optimizer` - Optimizer for peer limits
136/// * `cb` - Callbacks for progress and status updates
137///
138/// # Example
139///
140/// ```rust,no_run
141/// use kget::torrent::{download_magnet, TorrentCallbacks};
142/// use kget::{ProxyConfig, Optimizer};
143/// use std::sync::Arc;
144///
145/// // Simple download
146/// download_magnet(
147/// "magnet:?xt=urn:btih:HASH&dn=filename",
148/// "/home/user/Downloads",
149/// false,
150/// ProxyConfig::default(),
151/// Optimizer::new(),
152/// TorrentCallbacks::default(),
153/// ).unwrap();
154///
155/// // With progress tracking
156/// let callbacks = TorrentCallbacks {
157/// status: Some(Arc::new(|msg| log::info!("{}", msg))),
158/// progress: Some(Arc::new(|p| {
159/// update_progress_bar(p);
160/// })),
161/// };
162///
163/// download_magnet(
164/// "magnet:?xt=urn:btih:HASH",
165/// "./downloads",
166/// true, // quiet mode
167/// ProxyConfig::default(),
168/// Optimizer::new(),
169/// callbacks,
170/// ).unwrap();
171///
172/// fn update_progress_bar(p: f32) {
173/// // Update UI
174/// }
175/// ```
176///
177/// # Errors
178///
179/// Returns an error if:
180/// - Magnet link is invalid
181/// - Network connection fails
182/// - Output directory cannot be accessed
183/// - Download is interrupted
184pub fn download_magnet(
185 magnet: &str,
186 output_dir: &str,
187 quiet: bool,
188 proxy: ProxyConfig,
189 optimizer: Optimizer,
190 cb: TorrentCallbacks,
191) -> Result<(), Box<dyn Error + Send + Sync>> {
192 emit_progress(&cb, 0.0);
193
194 match selected_backend().as_str() {
195 "native" => {
196 #[cfg(feature = "torrent-native")]
197 {
198 return native::download_magnet_native(
199 magnet,
200 output_dir,
201 quiet,
202 proxy,
203 optimizer,
204 cb,
205 );
206 }
207
208 #[cfg(not(feature = "torrent-native"))]
209 {
210 emit_status(
211 &cb,
212 "Native torrent backend not available (compile with --features torrent-native). Falling back to external client.",
213 );
214 }
215 }
216 "transmission" => {
217 #[cfg(feature = "torrent-transmission")]
218 {
219 return transmission::download_via_transmission(
220 magnet,
221 output_dir,
222 quiet,
223 proxy,
224 optimizer,
225 cb,
226 );
227 }
228
229 #[cfg(not(feature = "torrent-transmission"))]
230 {
231 emit_status(
232 &cb,
233 "Torrent backend 'transmission' not available (compile with --features torrent-transmission). Falling back to external client.",
234 );
235 }
236 }
237 _ => {}
238 }
239
240 emit_status(
241 &cb,
242 format!(
243 "Opening magnet link in your default torrent client (output folder may be managed by that client): {}",
244 magnet
245 ),
246 );
247
248 external::open_magnet_in_default_client(magnet)?;
249 emit_progress(&cb, 1.0);
250 Ok(())
251}