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                            AdvancedDownloader::new(
113                                url.clone(),
114                                output_path.clone(),
115                                true,
116                                proxy,
117                                optimizer,
118                            )
119                            .and_then(|mut downloader| {
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                            })
139                        } else {
140                            let options = DownloadOptions {
141                                quiet_mode: true,
142                                output_path: Some(output_path.clone()),
143                                verify_iso,
144                                expected_sha256: expected_sha256.clone(),
145                                extra_headers: Vec::new(),
146                            };
147
148                            let status_tx_cb = status_tx_clone.clone();
149                            let status_cb = move |msg: String| {
150                                status_tx_cb
151                                    .send(WorkerToGuiMessage::StatusUpdate(msg))
152                                    .ok();
153                            };
154
155                            simple_download(&url, proxy, optimizer, options, Some(&status_cb))
156                        };
157
158                        report_download_result(
159                            result,
160                            output_path,
161                            &cancel_token_clone,
162                            &status_tx_clone,
163                        );
164                    }));
165                }
166                DownloadCommand::Cancel => {
167                    cancel_token.store(true, Ordering::SeqCst);
168                    let _ = status_tx.send(WorkerToGuiMessage::StatusUpdate(
169                        "Cancelling download...".into(),
170                    ));
171                }
172            },
173            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
174                if let Some(handle) = download_handle.take() {
175                    if handle.is_finished() {
176                        let _ = handle.join();
177                    } else {
178                        download_handle = Some(handle);
179                    }
180                }
181            }
182            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
183        }
184    }
185}
186
187fn report_download_result(
188    result: Result<(), Box<dyn Error + Send + Sync>>,
189    output_path: String,
190    cancel_token: &AtomicBool,
191    status_tx: &MpscSender<WorkerToGuiMessage>,
192) {
193    if cancel_token.load(Ordering::SeqCst) {
194        let _ = status_tx.send(WorkerToGuiMessage::StatusUpdate(
195            "Download cancelled".into(),
196        ));
197        return;
198    }
199
200    match result {
201        Ok(_) => {
202            let _ = status_tx.send(WorkerToGuiMessage::Completed(output_path));
203        }
204        Err(e) => {
205            let err_msg = e.to_string();
206            if err_msg.contains("cancelled") {
207                let _ = status_tx.send(WorkerToGuiMessage::StatusUpdate(
208                    "Download cancelled".into(),
209                ));
210            } else {
211                let _ = status_tx.send(WorkerToGuiMessage::Error(err_msg));
212            }
213        }
214    }
215}