1use colored::Colorize;
7use indicatif::{ProgressBar, ProgressStyle};
8use std::time::{Duration, Instant};
9
10pub struct TranscodeProgress {
19 bar: ProgressBar,
20 start_time: Instant,
21 frames_total: u64,
22 frames_done: u64,
23 bytes_written: u64,
24 last_update: Instant,
25 update_interval: Duration,
26}
27
28impl TranscodeProgress {
29 pub fn new(total_frames: u64) -> Self {
35 let bar = ProgressBar::new(total_frames);
36
37 let style = ProgressStyle::default_bar()
38 .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} frames ({percent}%) {msg}")
39 .expect("Failed to create progress style")
40 .progress_chars("=>-");
41
42 bar.set_style(style);
43
44 Self {
45 bar,
46 start_time: Instant::now(),
47 frames_total: total_frames,
48 frames_done: 0,
49 bytes_written: 0,
50 last_update: Instant::now(),
51 update_interval: Duration::from_millis(100),
52 }
53 }
54
55 pub fn new_spinner() -> Self {
59 let bar = ProgressBar::new_spinner();
60
61 let style = ProgressStyle::default_spinner()
62 .template("{spinner:.green} [{elapsed_precise}] {pos} frames {msg}")
63 .expect("Failed to create spinner style");
64
65 bar.set_style(style);
66
67 Self {
68 bar,
69 start_time: Instant::now(),
70 frames_total: 0,
71 frames_done: 0,
72 bytes_written: 0,
73 last_update: Instant::now(),
74 update_interval: Duration::from_millis(100),
75 }
76 }
77
78 pub fn update(&mut self, frames: u64) {
84 self.frames_done = frames;
85
86 let now = Instant::now();
88 if now.duration_since(self.last_update) < self.update_interval {
89 return;
90 }
91 self.last_update = now;
92
93 self.bar.set_position(frames);
94
95 let fps = self.fps();
97 let eta = self.eta();
98 let bitrate = self.bitrate();
99
100 let msg = format!(
101 "{:.1} fps | {} | {}",
102 fps,
103 format_eta(eta),
104 format_bitrate(bitrate)
105 );
106
107 self.bar.set_message(msg);
108 }
109
110 pub fn set_bytes_written(&mut self, bytes: u64) {
116 self.bytes_written = bytes;
117 }
118
119 #[allow(dead_code)]
125 pub fn set_status(&self, status: &str) {
126 self.bar.set_message(status.to_string());
127 }
128
129 pub fn finish(&self) {
131 let elapsed = self.start_time.elapsed();
132 let avg_fps = if elapsed.as_secs_f64() > 0.0 {
133 self.frames_done as f64 / elapsed.as_secs_f64()
134 } else {
135 0.0
136 };
137
138 let final_msg = format!(
139 "{} | Avg {:.1} fps | {}",
140 "Complete".green().bold(),
141 avg_fps,
142 format_size(self.bytes_written)
143 );
144
145 self.bar.finish_with_message(final_msg);
146 }
147
148 #[allow(dead_code)]
154 pub fn finish_with_error(&self, error: &str) {
155 let msg = format!("{} {}", "Failed:".red().bold(), error);
156 self.bar.finish_with_message(msg);
157 }
158
159 pub fn fps(&self) -> f64 {
161 let elapsed = self.start_time.elapsed();
162 if elapsed.as_secs_f64() > 0.0 {
163 self.frames_done as f64 / elapsed.as_secs_f64()
164 } else {
165 0.0
166 }
167 }
168
169 pub fn eta(&self) -> Duration {
171 if self.frames_total == 0 || self.frames_done == 0 {
172 return Duration::from_secs(0);
173 }
174
175 let elapsed = self.start_time.elapsed();
176 let frames_remaining = self.frames_total.saturating_sub(self.frames_done);
177
178 if self.frames_done > 0 {
179 let time_per_frame = elapsed.as_secs_f64() / self.frames_done as f64;
180 let eta_secs = time_per_frame * frames_remaining as f64;
181 Duration::from_secs_f64(eta_secs)
182 } else {
183 Duration::from_secs(0)
184 }
185 }
186
187 pub fn bitrate(&self) -> f64 {
189 let elapsed = self.start_time.elapsed();
190 if elapsed.as_secs_f64() > 0.0 {
191 (self.bytes_written as f64 * 8.0) / elapsed.as_secs_f64()
192 } else {
193 0.0
194 }
195 }
196
197 #[allow(dead_code)]
199 pub fn total_frames(&self) -> u64 {
200 self.frames_total
201 }
202
203 #[allow(dead_code)]
205 pub fn frames_completed(&self) -> u64 {
206 self.frames_done
207 }
208
209 #[allow(dead_code)]
211 pub fn elapsed(&self) -> Duration {
212 self.start_time.elapsed()
213 }
214}
215
216pub struct BatchProgress {
218 bar: ProgressBar,
219 start_time: Instant,
220 #[allow(dead_code)]
221 total_files: usize,
222 completed: usize,
223 failed: usize,
224}
225
226impl BatchProgress {
227 pub fn new(total_files: usize) -> Self {
233 let bar = ProgressBar::new(total_files as u64);
234
235 let style = ProgressStyle::default_bar()
236 .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} files ({percent}%) {msg}")
237 .expect("Failed to create progress style")
238 .progress_chars("=>-");
239
240 bar.set_style(style);
241
242 Self {
243 bar,
244 start_time: Instant::now(),
245 total_files,
246 completed: 0,
247 failed: 0,
248 }
249 }
250
251 pub fn inc_success(&mut self) {
253 self.completed += 1;
254 self.bar.inc(1);
255 self.update_message();
256 }
257
258 pub fn inc_failed(&mut self) {
260 self.failed += 1;
261 self.bar.inc(1);
262 self.update_message();
263 }
264
265 fn update_message(&self) {
267 let msg = if self.failed > 0 {
268 format!(
269 "{} succeeded, {} failed",
270 self.completed.to_string().green(),
271 self.failed.to_string().red()
272 )
273 } else {
274 format!("{} succeeded", self.completed.to_string().green())
275 };
276
277 self.bar.set_message(msg);
278 }
279
280 pub fn finish(&self) {
282 let elapsed = self.start_time.elapsed();
283 let msg = format!(
284 "{} | {} succeeded, {} failed | Took {}",
285 "Complete".green().bold(),
286 self.completed,
287 self.failed,
288 format_duration(elapsed)
289 );
290
291 self.bar.finish_with_message(msg);
292 }
293}
294
295fn format_duration(duration: Duration) -> String {
297 let total_secs = duration.as_secs();
298 let hours = total_secs / 3600;
299 let minutes = (total_secs % 3600) / 60;
300 let seconds = total_secs % 60;
301
302 if hours > 0 {
303 format!("{}h {}m {}s", hours, minutes, seconds)
304 } else if minutes > 0 {
305 format!("{}m {}s", minutes, seconds)
306 } else {
307 format!("{}s", seconds)
308 }
309}
310
311fn format_eta(eta: Duration) -> String {
313 let eta_str = format!("ETA {}", format_duration(eta));
314
315 if eta.as_secs() > 3600 {
316 eta_str.red().to_string()
317 } else if eta.as_secs() > 600 {
318 eta_str.yellow().to_string()
319 } else {
320 eta_str.green().to_string()
321 }
322}
323
324fn format_bitrate(bitrate: f64) -> String {
326 if bitrate >= 1_000_000.0 {
327 format!("{:.2} Mbps", bitrate / 1_000_000.0)
328 } else if bitrate >= 1_000.0 {
329 format!("{:.1} kbps", bitrate / 1_000.0)
330 } else {
331 format!("{:.0} bps", bitrate)
332 }
333}
334
335fn format_size(bytes: u64) -> String {
337 const KB: u64 = 1024;
338 const MB: u64 = KB * 1024;
339 const GB: u64 = MB * 1024;
340
341 if bytes >= GB {
342 format!("{:.2} GB", bytes as f64 / GB as f64)
343 } else if bytes >= MB {
344 format!("{:.2} MB", bytes as f64 / MB as f64)
345 } else if bytes >= KB {
346 format!("{:.2} KB", bytes as f64 / KB as f64)
347 } else {
348 format!("{} B", bytes)
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_format_duration() {
358 assert_eq!(format_duration(Duration::from_secs(30)), "30s");
359 assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
360 assert_eq!(format_duration(Duration::from_secs(3661)), "1h 1m 1s");
361 }
362
363 #[test]
364 fn test_format_bitrate() {
365 assert_eq!(format_bitrate(500.0), "500 bps");
366 assert_eq!(format_bitrate(1500.0), "1.5 kbps");
367 assert_eq!(format_bitrate(2_500_000.0), "2.50 Mbps");
368 }
369
370 #[test]
371 fn test_format_size() {
372 assert_eq!(format_size(500), "500 B");
373 assert_eq!(format_size(1536), "1.50 KB");
374 assert_eq!(format_size(2_097_152), "2.00 MB");
375 assert_eq!(format_size(1_610_612_736), "1.50 GB");
376 }
377
378 #[test]
379 fn test_progress_fps() {
380 let mut progress = TranscodeProgress::new(100);
381 std::thread::sleep(Duration::from_millis(100));
382 progress.update(10);
383
384 let fps = progress.fps();
385 assert!(fps > 0.0);
386 }
387
388 #[test]
389 fn test_progress_eta() {
390 let mut progress = TranscodeProgress::new(100);
391 std::thread::sleep(Duration::from_millis(100));
392 progress.update(10);
393
394 let eta = progress.eta();
395 let _ = eta.as_secs(); }
397}