1use std::error::Error;
2use std::time::Duration;
3use tokio::time::sleep;
4use url::Url;
5use transmission_rpc::{
6 types::{BasicAuth, TorrentAddArgs, Id, TorrentGetField, TorrentStatus},
7 TransClient,
8};
9use crate::config::ProxyConfig;
10use crate::optimization::Optimizer;
11use crate::progress::create_progress_bar;
12use crate::utils::print;
13#[cfg(feature = "gui")]
14use opener;
15
16pub struct TorrentDownloader {
17 url: String,
18 output: String,
19 quiet: bool,
20 proxy: ProxyConfig,
21 optimizer: Optimizer,
22}
23
24impl TorrentDownloader {
25 pub fn new(
26 url: String,
27 output: String,
28 quiet: bool,
29 proxy: ProxyConfig,
30 optimizer: Optimizer,
31 ) -> Self {
32 Self {
33 url,
34 output,
35 quiet,
36 proxy,
37 optimizer,
38 }
39 }
40
41 pub async fn download(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
42 let transmission_url = if self.proxy.enabled && self.proxy.url.is_some() {
44 let base_proxy_url = self.proxy.url.as_ref().unwrap(); if self.proxy.username.is_some() && self.proxy.password.is_some() {
47 format!("http://{}:{}@{}/transmission/rpc",
49 self.proxy.username.as_deref().unwrap_or(""),
50 self.proxy.password.as_deref().unwrap_or(""),
51 base_proxy_url.trim_start_matches("http://").trim_start_matches("https://") )
53 } else {
54 format!("http://{}/transmission/rpc",
56 base_proxy_url.trim_start_matches("http://").trim_start_matches("https://") )
58 }
59 } else {
60 "http://localhost:9091/transmission/rpc".to_string()
62 };
63
64 let mut client = TransClient::with_auth(
66 Url::parse(&transmission_url)?,
67 BasicAuth {
68 user: "transmission".into(), password: "transmission".into(), },
71 );
72
73 if !self.quiet {
75 print(&format!("Adding torrent: {}", self.url), self.quiet);
76 }
77
78 let args = TorrentAddArgs {
80 filename: Some(self.url.clone()),
81 download_dir: Some(self.output.clone()),
82 paused: Some(false),
83 peer_limit: Some(self.optimizer.get_peer_limit() as i64),
85 ..Default::default()
86 };
87
88 let response = client.torrent_add(args).await.map_err(|e| {
89 Box::<dyn Error + Send + Sync>::from(format!("Failed to add torrent: {}", e))
90 })?;
91
92 let torrent_id = match &response.arguments {
94 transmission_rpc::types::TorrentAddedOrDuplicate::TorrentAdded(added) => {
95 added.id.map(Id::Id).ok_or_else(|| Box::<dyn Error + Send + Sync>::from("TorrentAdded response missing ID"))?
96 },
97 transmission_rpc::types::TorrentAddedOrDuplicate::TorrentDuplicate(duplicate) => {
98 duplicate.id.map(Id::Id).ok_or_else(|| Box::<dyn Error + Send + Sync>::from("TorrentDuplicate response missing ID"))?
99 },
100 _ => { return Err(Box::<dyn Error + Send + Sync>::from("Failed to get torrent ID from response, unexpected variant"));
102 }
103 };
104
105 if !self.quiet {
107 let transmission_web_url = "http://localhost:9091/transmission/web/";
108 print(&format!("\nStarting the Download!\n"), self.quiet);
109 print(&format!("\nOpening Transmission web UI: {}\n", transmission_web_url), self.quiet);
110 #[cfg(feature = "gui")]
111 if let Err(e) = opener::open(transmission_web_url) {
112 print(&format!("Warning: Could not open web browser: {}", e), self.quiet);
113 }
115 #[cfg(not(feature = "gui"))]
116 {
117 print("GUI feature disabled; abra a URL manualmente se quiser gerenciar o download.", self.quiet);
119 }
120 }
121
122 let progress = create_progress_bar(
124 self.quiet,
125 "Downloading torrent".to_string(),
126 None,
127 false
128 );
129
130 let mut attempt_count = 0;
132 let max_attempts = 1800; loop {
135 if attempt_count >= max_attempts {
136 progress.finish_with_message("Download timeout or stalled."); return Err("Download timeout after 30 minutes or torrent stalled".into());
138 }
139
140 let torrent_info_result = client.torrent_get( Some(vec![
142 TorrentGetField::PercentDone,
143 TorrentGetField::Status,
144 TorrentGetField::Name,
145 TorrentGetField::RateDownload,
146 TorrentGetField::Eta,
147 TorrentGetField::Error, TorrentGetField::ErrorString,
149 ]),
150 Some(vec![torrent_id.clone()])
151 ).await;
152
153 if let Err(e) = torrent_info_result {
154 progress.abandon_with_message("Failed to get torrent info");
156 return Err(e);
157 }
158 let torrent_info = torrent_info_result.unwrap();
159
160
161 if let Some(t) = torrent_info.arguments.torrents.first() {
162 let percent_done = t.percent_done.unwrap_or(0.0);
163 let current_progress = (percent_done * 100.0) as u64;
164 progress.set_position(current_progress);
165
166 if let Some(name) = &t.name {
167 let speed_kb = t.rate_download.map_or(0, |rate| rate / 1024);
168 progress.set_message(format!(
169 "{} - {:.2}% - {} KB/s",
170 name,
171 percent_done * 100.0, speed_kb
173 ));
174 }
175
176 if let Some(error_code) = t.error {
178 if (error_code as i32) != 0 { let error_message = t.error_string.as_deref().unwrap_or("Unknown torrent error");
180 progress.abandon_with_message(format!("Torrent error: {}", error_message));
181 return Err(format!("Torrent error (code {:?}): {}", error_code, error_message).into());
182 }
183 }
184
185 if percent_done >= 1.0 {
187 progress.set_message(format!(
188 "{} - Complete",
189 t.name.as_deref().unwrap_or("Torrent")
190 ));
191 break; }
193
194 if let Some(status) = t.status {
196 match status {
197 TorrentStatus::Stopped => {
198 if attempt_count > 5 && percent_done < 1.0 { progress.abandon_with_message("Torrent stopped and not progressing.");
202 return Err(format!(
203 "Torrent '{}' stopped and not progressing.",
204 t.name.as_deref().unwrap_or("Unknown")
205 ).into());
206 }
207 }
209 TorrentStatus::Downloading => {
210 }
212 TorrentStatus::Seeding => {
213 }
216
217 _ => {
218 }
221 }
222 }
223 } else {
224 progress.abandon_with_message("Torrent info not found.");
226 return Err("Torrent info not found after adding.".into());
227 }
228
229 attempt_count += 1;
230 sleep(Duration::from_secs(1)).await;
231 }
232
233 progress.finish_with_message(format!(
234 "Download of '{}' completed successfully!",
235 client.torrent_get(Some(vec![TorrentGetField::Name]), Some(vec![torrent_id]))
237 .await
238 .ok()
239 .and_then(|resp| resp.arguments.torrents.first().and_then(|t| t.name.clone()))
240 .unwrap_or_else(|| "Torrent".to_string())
241 ));
242
243 if self.optimizer.is_compression_enabled() {
245 print("Optimizing downloaded files...", self.quiet);
246 }
248
249 Ok(())
250 }
251}