youtube_uploader/
progress.rs1use std::io::IsTerminal as _;
2
3use crate::{UploadError, UploadResult};
4
5pub trait ProgressListener: Send + Sync {
29 fn on_progress(&self, uploaded: u64, total: u64);
31
32 fn on_complete(&self, result: &UploadResult);
34
35 fn on_error(&self, error: &UploadError);
37}
38
39pub struct NoopProgressListener;
51
52impl ProgressListener for NoopProgressListener {
53 fn on_progress(&self, _uploaded: u64, _total: u64) {}
54 fn on_complete(&self, _result: &UploadResult) {}
55 fn on_error(&self, _error: &UploadError) {}
56}
57
58pub struct StderrProgressListener {
63 start: std::time::Instant,
64}
65
66impl Default for StderrProgressListener {
67 fn default() -> Self {
68 Self::new()
69 }
70}
71
72impl StderrProgressListener {
73 pub fn new() -> Self {
75 Self {
76 start: std::time::Instant::now(),
77 }
78 }
79}
80
81impl ProgressListener for StderrProgressListener {
82 fn on_progress(&self, uploaded: u64, total: u64) {
83 if total > 0 {
84 let pct = (uploaded as f64 / total as f64) * 100.0;
85 let elapsed = self.start.elapsed().as_secs_f64();
86
87 let speed_str = if elapsed > 0.0 && uploaded > 0 {
89 let speed = uploaded as f64 / elapsed;
90 format_speed(speed)
91 } else {
92 "--".to_string()
93 };
94
95 let eta_str = if uploaded > 0 && uploaded < total && elapsed > 0.0 {
96 let speed = uploaded as f64 / elapsed;
97 let remaining_bytes = total - uploaded;
98 let eta_secs = remaining_bytes as f64 / speed;
99 format_duration(eta_secs)
100 } else {
101 "--".to_string()
102 };
103
104 if std::io::stderr().is_terminal() {
105 eprint!(
106 "\r {:>6.2}% {}/s ETA {} ({}/{})",
107 pct, speed_str, eta_str, uploaded, total
108 );
109 } else {
110 eprintln!(
111 " {:>6.2}% {}/s ETA {} ({}/{})",
112 pct, speed_str, eta_str, uploaded, total
113 );
114 }
115 }
116 }
117
118 fn on_complete(&self, result: &UploadResult) {
119 let elapsed = self.start.elapsed();
120 if std::io::stderr().is_terminal() {
121 eprintln!(
122 "\n {} uploaded to {}: {}",
123 format_duration(elapsed.as_secs_f64()),
124 result.workspace,
125 result.url
126 );
127 } else {
128 eprintln!(
129 "[complete] {}: {} ({})",
130 result.workspace,
131 result.url,
132 format_duration(elapsed.as_secs_f64())
133 );
134 }
135 }
136
137 fn on_error(&self, error: &UploadError) {
138 eprintln!(" Upload failed: {}", error);
139 }
140}
141
142fn format_speed(bytes_per_sec: f64) -> String {
144 if bytes_per_sec >= 1_000_000_000.0 {
145 format!("{:.1} GB", bytes_per_sec / 1_000_000_000.0)
146 } else if bytes_per_sec >= 1_000_000.0 {
147 format!("{:.1} MB", bytes_per_sec / 1_000_000.0)
148 } else if bytes_per_sec >= 1_000.0 {
149 format!("{:.0} KB", bytes_per_sec / 1_000.0)
150 } else {
151 format!("{:.0} B", bytes_per_sec)
152 }
153}
154
155fn format_duration(secs: f64) -> String {
157 if secs.is_nan() || secs.is_infinite() || secs < 0.0 {
158 return "--".to_string();
159 }
160 let total_secs = secs as u64;
161 if total_secs < 60 {
162 format!("{}s", total_secs)
163 } else if total_secs < 3600 {
164 format!("{}m {}s", total_secs / 60, total_secs % 60)
165 } else {
166 format!("{}h {}m", total_secs / 3600, (total_secs % 3600) / 60)
167 }
168}