kget/torrent/
mod.rs

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        // Configure transmission URL with proxy if needed
43        let transmission_url = if self.proxy.enabled && self.proxy.url.is_some() {
44            // Só usa o proxy se estiver habilitado E uma URL de proxy for fornecida
45            let base_proxy_url = self.proxy.url.as_ref().unwrap(); // Sabemos que é Some
46            if self.proxy.username.is_some() && self.proxy.password.is_some() {
47                // Proxy com autenticação
48                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://") // Remove esquema se presente
52                )
53            } else {
54                // Proxy sem autenticação
55                format!("http://{}/transmission/rpc", 
56                    base_proxy_url.trim_start_matches("http://").trim_start_matches("https://") // Remove esquema se presente
57                )
58            }
59        } else {
60            // Proxy desabilitado ou URL do proxy não fornecida, usa o endereço padrão do Transmission
61            "http://localhost:9091/transmission/rpc".to_string()
62        };
63
64        // Create Transmission RPC client
65        let mut client = TransClient::with_auth(
66            Url::parse(&transmission_url)?,
67            BasicAuth {
68                user: "transmission".into(), // Usuário padrão do Transmission, ajuste se necessário
69                password: "transmission".into(), // Senha padrão do Transmission, ajuste se necessário
70            },
71        );
72
73        // Print status
74        if !self.quiet {
75            print(&format!("Adding torrent: {}", self.url), self.quiet);
76        }
77
78        // Add torrent
79        let args = TorrentAddArgs {
80            filename: Some(self.url.clone()),
81            download_dir: Some(self.output.clone()),
82            paused: Some(false),
83            // Apply optimization settings
84            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        // Extrai o torrent ID corretamente
93        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            _ => { // Este caso pode precisar de ajuste dependendo se há uma variante de Erro explícita
101                return Err(Box::<dyn Error + Send + Sync>::from("Failed to get torrent ID from response, unexpected variant"));
102            }
103        };
104
105        // Abrir a interface web do Transmission (somente se o feature `gui` estiver presente)
106        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                // Não retornar um erro aqui, pois o download pode prosseguir
114            }
115            #[cfg(not(feature = "gui"))]
116            {
117                // Quando compilado sem GUI, apenas informe a URL ao usuário.
118                print("GUI feature disabled; abra a URL manualmente se quiser gerenciar o download.", self.quiet);
119            }
120        }
121
122        // Setup progress bar
123        let progress = create_progress_bar(
124            self.quiet,
125            "Downloading torrent".to_string(),
126            None,
127            false
128        );
129
130        // Monitor download progress
131        let mut attempt_count = 0;
132        let max_attempts = 1800; // 30 minutes timeout (1800 seconds)
133        
134        loop {
135            if attempt_count >= max_attempts {
136                progress.finish_with_message("Download timeout or stalled."); // Mensagem mais informativa
137                return Err("Download timeout after 30 minutes or torrent stalled".into());
138            }
139            
140            let torrent_info_result = client.torrent_get( // Renomeado para evitar sombreamento
141                Some(vec![
142                    TorrentGetField::PercentDone,
143                    TorrentGetField::Status,
144                    TorrentGetField::Name,
145                    TorrentGetField::RateDownload,
146                    TorrentGetField::Eta,
147                    TorrentGetField::Error, // Adicionar campo de erro para verificar erros do daemon
148                    TorrentGetField::ErrorString,
149                ]),
150                Some(vec![torrent_id.clone()])
151            ).await;
152
153            if let Err(e) = torrent_info_result {
154                // Se falhar ao obter informações do torrent, pode ser um problema de conexão
155                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, // Usar float para precisão na mensagem
172                        speed_kb
173                    ));
174                }
175
176                // Verificar se há um erro reportado pelo daemon para este torrent
177                if let Some(error_code) = t.error {
178                    if (error_code as i32) != 0 { // 0 geralmente significa sem erro
179                        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                // Check if download is complete
186                if percent_done >= 1.0 {
187                    progress.set_message(format!(
188                        "{} - Complete",
189                        t.name.as_deref().unwrap_or("Torrent")
190                    ));
191                    break; // Sai do loop se completo
192                }
193                
194                // Check for torrent status
195                if let Some(status) = t.status {
196                    match status {
197                        TorrentStatus::Stopped => {
198                            // Se estiver parado, mas não completo, pode ser um problema ou apenas o estado inicial de um duplicado.
199                            // Damos algumas tentativas para ver se ele inicia.
200                            if attempt_count > 5 && percent_done < 1.0 { // Após 5 segundos, se ainda parado e não completo
201                                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                            // Se estiver parado e completo, o break acima já teria sido acionado.
208                        }
209                        TorrentStatus::Downloading => { 
210                            // Tudo ok, está baixando
211                        }
212                        TorrentStatus::Seeding => {    
213                            // Se estiver semeando, e percent_done < 1.0, algo está estranho, mas vamos deixar o loop de percent_done tratar.
214                            // Se percent_done >= 1.0, o break acima trata.
215                        }
216                        
217                        _ => {
218                            // Para outros status não explicitamente tratados (como DownloadWait, SeedWait)
219                            // você pode querer apenas continuar ou logar, dependendo da sua lógica.
220                        }
221                    }
222                }
223            } else {
224                // Não deveria acontecer se o ID do torrent for válido
225                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            // Tenta obter o nome do torrent uma última vez para a mensagem final
236            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        // Apply optimizer if needed
244        if self.optimizer.is_compression_enabled() {
245            print("Optimizing downloaded files...", self.quiet);
246            // Implement compression here if needed
247        }
248
249        Ok(())
250    }
251}