Skip to main content

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}