audiobook_forge/core/
progress.rs1use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
4use std::sync::Arc;
5use std::time::Instant;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ProcessingStage {
10 Scanning,
12 Analyzing,
14 Processing,
16 Chapters,
18 Metadata,
20 Complete,
22}
23
24impl ProcessingStage {
25 pub fn name(&self) -> &'static str {
27 match self {
28 Self::Scanning => "Scanning",
29 Self::Analyzing => "Analyzing",
30 Self::Processing => "Processing",
31 Self::Chapters => "Chapters",
32 Self::Metadata => "Metadata",
33 Self::Complete => "Complete",
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct BookProgress {
41 pub name: String,
43 pub stage: ProcessingStage,
45 pub progress: f32,
47 pub start_time: Instant,
49 pub eta_seconds: Option<f64>,
51}
52
53impl BookProgress {
54 pub fn new(name: String) -> Self {
56 Self {
57 name,
58 stage: ProcessingStage::Scanning,
59 progress: 0.0,
60 start_time: Instant::now(),
61 eta_seconds: None,
62 }
63 }
64
65 pub fn set_stage(&mut self, stage: ProcessingStage) {
67 self.stage = stage;
68 }
69
70 pub fn set_progress(&mut self, progress: f32) {
72 self.progress = progress.clamp(0.0, 100.0);
73 }
74
75 pub fn update_eta(&mut self) {
77 if self.progress > 0.0 {
78 let elapsed = self.start_time.elapsed().as_secs_f64();
79 let total_estimated = elapsed / (self.progress as f64 / 100.0);
80 self.eta_seconds = Some(total_estimated - elapsed);
81 }
82 }
83
84 pub fn elapsed_seconds(&self) -> f64 {
86 self.start_time.elapsed().as_secs_f64()
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct BatchProgress {
93 total_books: usize,
95 completed: Arc<AtomicUsize>,
97 failed: Arc<AtomicUsize>,
99 bytes_processed: Arc<AtomicU64>,
101 start_time: Instant,
103}
104
105impl BatchProgress {
106 pub fn new(total_books: usize) -> Self {
108 Self {
109 total_books,
110 completed: Arc::new(AtomicUsize::new(0)),
111 failed: Arc::new(AtomicUsize::new(0)),
112 bytes_processed: Arc::new(AtomicU64::new(0)),
113 start_time: Instant::now(),
114 }
115 }
116
117 pub fn mark_completed(&self) {
119 self.completed.fetch_add(1, Ordering::Relaxed);
120 }
121
122 pub fn mark_failed(&self) {
124 self.failed.fetch_add(1, Ordering::Relaxed);
125 }
126
127 pub fn add_bytes(&self, bytes: u64) {
129 self.bytes_processed.fetch_add(bytes, Ordering::Relaxed);
130 }
131
132 pub fn completed_count(&self) -> usize {
134 self.completed.load(Ordering::Relaxed)
135 }
136
137 pub fn failed_count(&self) -> usize {
139 self.failed.load(Ordering::Relaxed)
140 }
141
142 pub fn total_bytes(&self) -> u64 {
144 self.bytes_processed.load(Ordering::Relaxed)
145 }
146
147 pub fn total_books(&self) -> usize {
149 self.total_books
150 }
151
152 pub fn overall_progress(&self) -> f32 {
154 if self.total_books == 0 {
155 return 0.0;
156 }
157
158 let processed = self.completed_count() + self.failed_count();
159 (processed as f32 / self.total_books as f32) * 100.0
160 }
161
162 pub fn eta_seconds(&self) -> Option<f64> {
164 let completed = self.completed_count();
165 if completed == 0 {
166 return None;
167 }
168
169 let elapsed = self.start_time.elapsed().as_secs_f64();
170 let avg_time_per_book = elapsed / completed as f64;
171 let remaining_books = self.total_books - completed - self.failed_count();
172
173 if remaining_books > 0 {
174 Some(avg_time_per_book * remaining_books as f64)
175 } else {
176 Some(0.0)
177 }
178 }
179
180 pub fn elapsed_seconds(&self) -> f64 {
182 self.start_time.elapsed().as_secs_f64()
183 }
184
185 pub fn format_eta(&self) -> String {
187 match self.eta_seconds() {
188 Some(seconds) if seconds > 0.0 => {
189 let hours = (seconds / 3600.0) as u64;
190 let minutes = ((seconds % 3600.0) / 60.0) as u64;
191 let secs = (seconds % 60.0) as u64;
192
193 if hours > 0 {
194 format!("{}h {:02}m {:02}s", hours, minutes, secs)
195 } else if minutes > 0 {
196 format!("{}m {:02}s", minutes, secs)
197 } else {
198 format!("{}s", secs)
199 }
200 }
201 _ => "calculating...".to_string(),
202 }
203 }
204
205 pub fn format_elapsed(&self) -> String {
207 let seconds = self.elapsed_seconds();
208 let hours = (seconds / 3600.0) as u64;
209 let minutes = ((seconds % 3600.0) / 60.0) as u64;
210 let secs = (seconds % 60.0) as u64;
211
212 if hours > 0 {
213 format!("{}h {:02}m {:02}s", hours, minutes, secs)
214 } else if minutes > 0 {
215 format!("{}m {:02}s", minutes, secs)
216 } else {
217 format!("{}s", secs)
218 }
219 }
220
221 pub fn is_complete(&self) -> bool {
223 self.completed_count() + self.failed_count() >= self.total_books
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use std::thread;
231 use std::time::Duration;
232
233 #[test]
234 fn test_processing_stage_name() {
235 assert_eq!(ProcessingStage::Scanning.name(), "Scanning");
236 assert_eq!(ProcessingStage::Analyzing.name(), "Analyzing");
237 assert_eq!(ProcessingStage::Complete.name(), "Complete");
238 }
239
240 #[test]
241 fn test_book_progress() {
242 let mut progress = BookProgress::new("Test Book".to_string());
243 assert_eq!(progress.name, "Test Book");
244 assert_eq!(progress.stage, ProcessingStage::Scanning);
245 assert_eq!(progress.progress, 0.0);
246
247 progress.set_stage(ProcessingStage::Processing);
248 assert_eq!(progress.stage, ProcessingStage::Processing);
249
250 progress.set_progress(50.0);
251 assert_eq!(progress.progress, 50.0);
252
253 progress.set_progress(150.0);
255 assert_eq!(progress.progress, 100.0);
256 }
257
258 #[test]
259 fn test_batch_progress() {
260 let progress = BatchProgress::new(10);
261 assert_eq!(progress.total_books(), 10);
262 assert_eq!(progress.completed_count(), 0);
263 assert_eq!(progress.failed_count(), 0);
264 assert_eq!(progress.overall_progress(), 0.0);
265
266 progress.mark_completed();
267 assert_eq!(progress.completed_count(), 1);
268 assert_eq!(progress.overall_progress(), 10.0);
269
270 progress.mark_failed();
271 assert_eq!(progress.failed_count(), 1);
272 assert_eq!(progress.overall_progress(), 20.0);
273 }
274
275 #[test]
276 fn test_batch_progress_bytes() {
277 let progress = BatchProgress::new(5);
278 assert_eq!(progress.total_bytes(), 0);
279
280 progress.add_bytes(1024);
281 assert_eq!(progress.total_bytes(), 1024);
282
283 progress.add_bytes(2048);
284 assert_eq!(progress.total_bytes(), 3072);
285 }
286
287 #[test]
288 fn test_batch_progress_eta() {
289 let progress = BatchProgress::new(10);
290
291 assert!(progress.eta_seconds().is_none());
293
294 thread::sleep(Duration::from_millis(100));
296
297 progress.mark_completed();
299
300 assert!(progress.eta_seconds().is_some());
302 }
303
304 #[test]
305 fn test_batch_progress_is_complete() {
306 let progress = BatchProgress::new(3);
307 assert!(!progress.is_complete());
308
309 progress.mark_completed();
310 progress.mark_completed();
311 assert!(!progress.is_complete());
312
313 progress.mark_completed();
314 assert!(progress.is_complete());
315 }
316
317 #[test]
318 fn test_format_eta() {
319 let progress = BatchProgress::new(1);
320 let eta = progress.format_eta();
321 assert_eq!(eta, "calculating...");
322 }
323
324 #[test]
325 fn test_format_elapsed() {
326 let progress = BatchProgress::new(1);
327 thread::sleep(Duration::from_millis(100));
328 let elapsed = progress.format_elapsed();
329 assert!(elapsed.ends_with('s'));
330 }
331}