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 {
114 "native".to_string()
115 }
116 #[cfg(not(feature = "torrent-native"))]
117 {
118 "external".to_string()
119 }
120 })
121 .to_lowercase()
122}
123
124/// Return true when a magnet link looks like a supported BitTorrent magnet.
125pub fn is_supported_magnet_link(magnet: &str) -> bool {
126 let lower = magnet.to_ascii_lowercase();
127 lower.starts_with("magnet:?")
128 && (lower.contains("xt=urn:btih:") || lower.contains("xt=urn:btmh:"))
129}
130
131/// Download a torrent from a magnet link.
132///
133/// This function automatically selects the best available backend:
134/// 1. Native client (if `torrent-native` feature is enabled)
135/// 2. Transmission RPC (if `torrent-transmission` feature is enabled)
136/// 3. External client (opens in system default torrent app)
137///
138/// Override the backend with `KGET_TORRENT_BACKEND` environment variable.
139///
140/// # Arguments
141///
142/// * `magnet` - Magnet link starting with `magnet:?`
143/// * `output_dir` - Directory to save downloaded files
144/// * `quiet` - Suppress console output
145/// * `proxy` - Proxy configuration (native backend only)
146/// * `optimizer` - Optimizer for peer limits
147/// * `cb` - Callbacks for progress and status updates
148///
149/// # Example
150///
151/// ```rust,no_run
152/// use kget::torrent::{download_magnet, TorrentCallbacks};
153/// use kget::{ProxyConfig, Optimizer};
154/// use std::sync::Arc;
155///
156/// // Simple download
157/// download_magnet(
158/// "magnet:?xt=urn:btih:HASH&dn=filename",
159/// "/home/user/Downloads",
160/// false,
161/// ProxyConfig::default(),
162/// Optimizer::new(),
163/// TorrentCallbacks::default(),
164/// ).unwrap();
165///
166/// // With progress tracking
167/// let callbacks = TorrentCallbacks {
168/// status: Some(Arc::new(|msg| println!("{}", msg))),
169/// progress: Some(Arc::new(|p| {
170/// update_progress_bar(p);
171/// })),
172/// };
173///
174/// download_magnet(
175/// "magnet:?xt=urn:btih:HASH",
176/// "./downloads",
177/// true, // quiet mode
178/// ProxyConfig::default(),
179/// Optimizer::new(),
180/// callbacks,
181/// ).unwrap();
182///
183/// fn update_progress_bar(p: f32) {
184/// // Update UI
185/// }
186/// ```
187///
188/// # Errors
189///
190/// Returns an error if:
191/// - Magnet link is invalid
192/// - Network connection fails
193/// - Output directory cannot be accessed
194/// - Download is interrupted
195pub fn download_magnet(
196 magnet: &str,
197 _output_dir: &str,
198 _quiet: bool,
199 _proxy: ProxyConfig,
200 _optimizer: Optimizer,
201 cb: TorrentCallbacks,
202) -> Result<(), Box<dyn Error + Send + Sync>> {
203 if !is_supported_magnet_link(magnet) {
204 return Err("Invalid or unsupported BitTorrent magnet link".into());
205 }
206
207 emit_progress(&cb, 0.0);
208
209 match selected_backend().as_str() {
210 "native" => {
211 #[cfg(feature = "torrent-native")]
212 {
213 return native::download_magnet_native(
214 magnet,
215 _output_dir,
216 _quiet,
217 _proxy,
218 _optimizer,
219 cb,
220 );
221 }
222
223 #[cfg(not(feature = "torrent-native"))]
224 {
225 emit_status(
226 &cb,
227 "Native torrent backend not available (compile with --features torrent-native). Falling back to external client.",
228 );
229 }
230 }
231 "transmission" => {
232 #[cfg(feature = "torrent-transmission")]
233 {
234 return transmission::download_via_transmission(
235 magnet,
236 _output_dir,
237 _quiet,
238 _proxy,
239 _optimizer,
240 cb,
241 );
242 }
243
244 #[cfg(not(feature = "torrent-transmission"))]
245 {
246 emit_status(
247 &cb,
248 "Torrent backend 'transmission' not available (compile with --features torrent-transmission). Falling back to external client.",
249 );
250 }
251 }
252 _ => {}
253 }
254
255 emit_status(
256 &cb,
257 format!(
258 "Opening magnet link in your default torrent client (output folder may be managed by that client): {}",
259 magnet
260 ),
261 );
262
263 external::open_magnet_in_default_client(magnet)?;
264 emit_progress(&cb, 1.0);
265 Ok(())
266}