Skip to main content

steamroom_cli/
download.rs

1use std::sync::Arc;
2use std::time::Instant;
3
4use indicatif::MultiProgress;
5use indicatif::ProgressBar;
6use indicatif::ProgressStyle;
7use steamroom_client::event::DownloadEvent;
8use tokio::sync::mpsc;
9use tokio::task::JoinHandle;
10
11use crate::daemon::proto::ProgressUpdate;
12use crate::sink::JobSink;
13
14pub fn spawn_progress_renderer(
15    mut rx: mpsc::UnboundedReceiver<DownloadEvent>,
16    show_bars: bool,
17    sink: Option<Arc<dyn JobSink>>,
18) -> JoinHandle<()> {
19    tokio::spawn(async move {
20        if show_bars {
21            run_with_bars(&mut rx, sink).await;
22        } else {
23            run_quiet(&mut rx, sink).await;
24        }
25    })
26}
27
28async fn run_with_bars(
29    rx: &mut mpsc::UnboundedReceiver<DownloadEvent>,
30    sink: Option<Arc<dyn JobSink>>,
31) {
32    let mp = MultiProgress::new();
33
34    let total_bar = mp.add(ProgressBar::hidden());
35    let file_bar = mp.add(ProgressBar::new_spinner());
36    file_bar.set_style(
37        ProgressStyle::default_spinner()
38            .template("{spinner:.green} {wide_msg}")
39            .unwrap(),
40    );
41
42    let start_time = Instant::now();
43    let mut bytes_done: u64 = 0;
44    let mut bytes_total: u64 = 0;
45    let mut files_done: u32 = 0;
46    let mut files_total: u32 = 0;
47    let mut last_sink_send = Instant::now();
48
49    while let Some(event) = rx.recv().await {
50        match event {
51            DownloadEvent::DownloadStarted {
52                total_bytes,
53                total_files,
54            } => {
55                bytes_total = total_bytes;
56                files_total = total_files as u32;
57                total_bar.set_length(total_bytes);
58                total_bar.set_style(
59                    ProgressStyle::default_bar()
60                        .template("{spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
61                        .unwrap()
62                        .progress_chars("=> "),
63                );
64                total_bar.reset();
65                mp.println(format!(
66                    "downloading {total_files} files ({})",
67                    fmt_bytes(total_bytes)
68                ))
69                .ok();
70            }
71            DownloadEvent::FileStarted { filename } => {
72                file_bar.set_message(filename);
73            }
74            DownloadEvent::FileCompleted { filename } => {
75                files_done += 1;
76                file_bar.set_message(filename);
77                if let Some(ref s) = sink {
78                    let elapsed = start_time.elapsed().as_secs_f64();
79                    let rate = if elapsed > 0.0 {
80                        bytes_done as f64 / elapsed
81                    } else {
82                        0.0
83                    };
84                    let remaining = bytes_total.saturating_sub(bytes_done) as f64;
85                    let eta = if rate > 0.0 { remaining / rate } else { 0.0 };
86                    s.progress(ProgressUpdate {
87                        bytes_done,
88                        bytes_total,
89                        files_done,
90                        files_total,
91                        rate_bytes_per_sec: rate as u64,
92                        eta_seconds: eta as u32,
93                    });
94                    last_sink_send = Instant::now();
95                }
96            }
97            DownloadEvent::FileSkipped { .. } => {}
98            DownloadEvent::FileRemoved { filename } => {
99                mp.println(format!("removed {filename}")).ok();
100            }
101            DownloadEvent::ChunkCompleted { bytes } => {
102                bytes_done += bytes;
103                total_bar.inc(bytes);
104                if let Some(ref s) = sink {
105                    let now = Instant::now();
106                    if now.duration_since(last_sink_send) >= std::time::Duration::from_millis(100) {
107                        let elapsed = start_time.elapsed().as_secs_f64();
108                        let rate = if elapsed > 0.0 {
109                            bytes_done as f64 / elapsed
110                        } else {
111                            0.0
112                        };
113                        let remaining = bytes_total.saturating_sub(bytes_done) as f64;
114                        let eta = if rate > 0.0 { remaining / rate } else { 0.0 };
115                        s.progress(ProgressUpdate {
116                            bytes_done,
117                            bytes_total,
118                            files_done,
119                            files_total,
120                            rate_bytes_per_sec: rate as u64,
121                            eta_seconds: eta as u32,
122                        });
123                        last_sink_send = now;
124                    }
125                }
126            }
127            DownloadEvent::ChunkFailed { error } => {
128                mp.println(format!("warning: chunk failed (retrying): {error}"))
129                    .ok();
130            }
131            _ => {}
132        }
133    }
134
135    total_bar.finish_and_clear();
136    file_bar.finish_and_clear();
137}
138
139async fn run_quiet(
140    rx: &mut mpsc::UnboundedReceiver<DownloadEvent>,
141    sink: Option<Arc<dyn JobSink>>,
142) {
143    let start_time = Instant::now();
144    let mut bytes_done: u64 = 0;
145    let mut bytes_total: u64 = 0;
146    let mut files_done: u32 = 0;
147    let mut files_total: u32 = 0;
148    let mut last_sink_send = Instant::now();
149
150    while let Some(event) = rx.recv().await {
151        match event {
152            DownloadEvent::DownloadStarted {
153                total_bytes,
154                total_files,
155            } => {
156                bytes_total = total_bytes;
157                files_total = total_files as u32;
158            }
159            DownloadEvent::FileCompleted { filename } => {
160                files_done += 1;
161                let pct = if bytes_total > 0 {
162                    bytes_done as f64 / bytes_total as f64 * 100.0
163                } else {
164                    0.0
165                };
166                tracing::info!("[{pct:.1}%] {filename}");
167                if let Some(ref s) = sink {
168                    let elapsed = start_time.elapsed().as_secs_f64();
169                    let rate = if elapsed > 0.0 {
170                        bytes_done as f64 / elapsed
171                    } else {
172                        0.0
173                    };
174                    let remaining = bytes_total.saturating_sub(bytes_done) as f64;
175                    let eta = if rate > 0.0 { remaining / rate } else { 0.0 };
176                    s.progress(ProgressUpdate {
177                        bytes_done,
178                        bytes_total,
179                        files_done,
180                        files_total,
181                        rate_bytes_per_sec: rate as u64,
182                        eta_seconds: eta as u32,
183                    });
184                    last_sink_send = Instant::now();
185                }
186            }
187            DownloadEvent::ChunkCompleted { bytes } => {
188                bytes_done += bytes;
189                if let Some(ref s) = sink {
190                    let now = Instant::now();
191                    if now.duration_since(last_sink_send) >= std::time::Duration::from_millis(100) {
192                        let elapsed = start_time.elapsed().as_secs_f64();
193                        let rate = if elapsed > 0.0 {
194                            bytes_done as f64 / elapsed
195                        } else {
196                            0.0
197                        };
198                        let remaining = bytes_total.saturating_sub(bytes_done) as f64;
199                        let eta = if rate > 0.0 { remaining / rate } else { 0.0 };
200                        s.progress(ProgressUpdate {
201                            bytes_done,
202                            bytes_total,
203                            files_done,
204                            files_total,
205                            rate_bytes_per_sec: rate as u64,
206                            eta_seconds: eta as u32,
207                        });
208                        last_sink_send = now;
209                    }
210                }
211            }
212            DownloadEvent::FileRemoved { filename } => {
213                tracing::info!("removed {filename}");
214            }
215            DownloadEvent::ChunkFailed { error } => {
216                tracing::warn!("chunk failed (retrying): {error}");
217            }
218            _ => {}
219        }
220    }
221}
222
223fn fmt_bytes(bytes: u64) -> String {
224    const KB: u64 = 1024;
225    const MB: u64 = 1024 * KB;
226    const GB: u64 = 1024 * MB;
227    if bytes >= GB {
228        format!("{:.1} GB", bytes as f64 / GB as f64)
229    } else if bytes >= MB {
230        format!("{:.1} MB", bytes as f64 / MB as f64)
231    } else if bytes >= KB {
232        format!("{:.1} KB", bytes as f64 / KB as f64)
233    } else {
234        format!("{bytes} B")
235    }
236}