Skip to main content

kget/
app.rs

1//! Application-facing orchestration shared by GUI frontends.
2//!
3//! This module keeps UI worker plumbing out of `main.rs` so the binary can stay
4//! thin and future frontends can reuse the same command/message contract.
5
6use crate::DownloadOptions;
7use crate::advanced_download::AdvancedDownloader;
8use crate::config::Config;
9use crate::download::download as simple_download;
10use crate::optimization::Optimizer;
11use crate::torrent::{TorrentCallbacks, download_magnet};
12use std::error::Error;
13use std::fmt;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::mpsc::{Receiver as MpscReceiver, Sender as MpscSender};
17use std::thread;
18use std::time::Duration;
19
20/// Command sent from a frontend to the download worker.
21#[derive(Debug, Clone)]
22pub enum DownloadCommand {
23    Start {
24        url: String,
25        output_path: String,
26        is_advanced: bool,
27        verify_iso: bool,
28    },
29    Cancel,
30}
31
32/// Message sent from the download worker back to a frontend.
33#[derive(Debug, Clone)]
34pub enum WorkerToGuiMessage {
35    Progress(f32),
36    StatusUpdate(String),
37    Completed(String),
38    Error(String),
39}
40
41impl fmt::Display for WorkerToGuiMessage {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            WorkerToGuiMessage::Progress(p) => write!(f, "Progress({:.2})", p),
45            WorkerToGuiMessage::StatusUpdate(s) => write!(f, "Status: {}", s),
46            WorkerToGuiMessage::Completed(s) => write!(f, "Completed: {}", s),
47            WorkerToGuiMessage::Error(e) => write!(f, "Error: {}", e),
48        }
49    }
50}
51
52/// Spawn the blocking download worker used by the desktop GUI.
53pub fn spawn_download_worker(
54    config: Config,
55    download_rx: MpscReceiver<DownloadCommand>,
56    status_tx: MpscSender<WorkerToGuiMessage>,
57) -> thread::JoinHandle<()> {
58    thread::spawn(move || {
59        download_worker(config, download_rx, status_tx);
60    })
61}
62
63fn download_worker(
64    config: Config,
65    download_rx: MpscReceiver<DownloadCommand>,
66    status_tx: MpscSender<WorkerToGuiMessage>,
67) {
68    let cancel_token = Arc::new(AtomicBool::new(false));
69    let mut download_handle: Option<thread::JoinHandle<()>> = None;
70
71    loop {
72        match download_rx.recv_timeout(Duration::from_millis(50)) {
73            Ok(command) => match command {
74                DownloadCommand::Start {
75                    url,
76                    output_path,
77                    is_advanced,
78                    verify_iso,
79                } => {
80                    cancel_token.store(false, Ordering::SeqCst);
81                    let _ = status_tx.send(WorkerToGuiMessage::StatusUpdate(format!(
82                        "Initializing: {}",
83                        url
84                    )));
85
86                    let optimizer = Optimizer::from_config(config.optimization.clone());
87                    let proxy = config.proxy.clone();
88                    let cancel_token_clone = cancel_token.clone();
89                    let status_tx_clone = status_tx.clone();
90
91                    download_handle = Some(thread::spawn(move || {
92                        let result = if url.starts_with("magnet:?") {
93                            let callbacks = TorrentCallbacks {
94                                status: Some(Arc::new({
95                                    let status_tx = status_tx_clone.clone();
96                                    move |msg| {
97                                        status_tx.send(WorkerToGuiMessage::StatusUpdate(msg)).ok();
98                                    }
99                                })),
100                                progress: Some(Arc::new({
101                                    let status_tx = status_tx_clone.clone();
102                                    move |p| {
103                                        status_tx.send(WorkerToGuiMessage::Progress(p)).ok();
104                                    }
105                                })),
106                            };
107
108                            download_magnet(&url, &output_path, true, proxy, optimizer, callbacks)
109                        } else if is_advanced {
110                            let mut downloader = AdvancedDownloader::new(
111                                url.clone(),
112                                output_path.clone(),
113                                true,
114                                proxy,
115                                optimizer,
116                            );
117
118                            downloader.set_cancel_token(cancel_token_clone.clone());
119
120                            let status_tx_cb = status_tx_clone.clone();
121                            downloader.set_progress_callback(move |p| {
122                                status_tx_cb.send(WorkerToGuiMessage::Progress(p)).ok();
123                            });
124
125                            let status_tx_cb = status_tx_clone.clone();
126                            downloader.set_status_callback(move |msg| {
127                                status_tx_cb
128                                    .send(WorkerToGuiMessage::StatusUpdate(msg))
129                                    .ok();
130                            });
131
132                            downloader.download()
133                        } else {
134                            let options = DownloadOptions {
135                                quiet_mode: true,
136                                output_path: Some(output_path.clone()),
137                                verify_iso,
138                                expected_sha256: None,
139                            };
140
141                            let status_tx_cb = status_tx_clone.clone();
142                            let status_cb = move |msg: String| {
143                                status_tx_cb
144                                    .send(WorkerToGuiMessage::StatusUpdate(msg))
145                                    .ok();
146                            };
147
148                            simple_download(&url, proxy, optimizer, options, Some(&status_cb))
149                        };
150
151                        report_download_result(
152                            result,
153                            output_path,
154                            &cancel_token_clone,
155                            &status_tx_clone,
156                        );
157                    }));
158                }
159                DownloadCommand::Cancel => {
160                    cancel_token.store(true, Ordering::SeqCst);
161                    let _ = status_tx.send(WorkerToGuiMessage::StatusUpdate(
162                        "Cancelling download...".into(),
163                    ));
164                }
165            },
166            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
167                if let Some(handle) = download_handle.take() {
168                    if handle.is_finished() {
169                        let _ = handle.join();
170                    } else {
171                        download_handle = Some(handle);
172                    }
173                }
174            }
175            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
176        }
177    }
178}
179
180fn report_download_result(
181    result: Result<(), Box<dyn Error + Send + Sync>>,
182    output_path: String,
183    cancel_token: &AtomicBool,
184    status_tx: &MpscSender<WorkerToGuiMessage>,
185) {
186    if cancel_token.load(Ordering::SeqCst) {
187        let _ = status_tx.send(WorkerToGuiMessage::StatusUpdate(
188            "Download cancelled".into(),
189        ));
190        return;
191    }
192
193    match result {
194        Ok(_) => {
195            let _ = status_tx.send(WorkerToGuiMessage::Completed(output_path));
196        }
197        Err(e) => {
198            let err_msg = e.to_string();
199            if err_msg.contains("cancelled") {
200                let _ = status_tx.send(WorkerToGuiMessage::StatusUpdate(
201                    "Download cancelled".into(),
202                ));
203            } else {
204                let _ = status_tx.send(WorkerToGuiMessage::Error(err_msg));
205            }
206        }
207    }
208}