steamroom_cli/
download.rs1use 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}