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