oximedia_conform/
progress.rs1use serde::{Deserialize, Serialize};
4use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum ProgressStage {
11 LoadingTimeline,
13 ScanningMedia,
15 MatchingClips,
17 Validating,
19 Conforming,
21 Exporting,
23 Complete,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ProgressInfo {
30 pub stage: ProgressStage,
32 pub current_item: usize,
34 pub total_items: usize,
36 pub percentage: f64,
38 pub eta_seconds: Option<f64>,
40 pub description: String,
42}
43
44pub struct ProgressTracker {
46 stage: Arc<parking_lot::RwLock<ProgressStage>>,
48 current_item: Arc<AtomicUsize>,
50 total_items: Arc<AtomicUsize>,
52 bytes_processed: Arc<AtomicU64>,
54 total_bytes: Arc<AtomicU64>,
56 start_time: Instant,
58 description: Arc<parking_lot::RwLock<String>>,
60}
61
62impl ProgressTracker {
63 #[must_use]
65 pub fn new() -> Self {
66 Self {
67 stage: Arc::new(parking_lot::RwLock::new(ProgressStage::LoadingTimeline)),
68 current_item: Arc::new(AtomicUsize::new(0)),
69 total_items: Arc::new(AtomicUsize::new(0)),
70 bytes_processed: Arc::new(AtomicU64::new(0)),
71 total_bytes: Arc::new(AtomicU64::new(0)),
72 start_time: Instant::now(),
73 description: Arc::new(parking_lot::RwLock::new(String::new())),
74 }
75 }
76
77 pub fn set_stage(&self, stage: ProgressStage) {
79 *self.stage.write() = stage;
80 }
81
82 #[must_use]
84 pub fn stage(&self) -> ProgressStage {
85 *self.stage.read()
86 }
87
88 pub fn set_total_items(&self, total: usize) {
90 self.total_items.store(total, Ordering::Relaxed);
91 }
92
93 pub fn set_current_item(&self, current: usize) {
95 self.current_item.store(current, Ordering::Relaxed);
96 }
97
98 pub fn increment_item(&self) {
100 self.current_item.fetch_add(1, Ordering::Relaxed);
101 }
102
103 pub fn set_description(&self, desc: String) {
105 *self.description.write() = desc;
106 }
107
108 #[must_use]
110 pub fn percentage(&self) -> f64 {
111 let total = self.total_items.load(Ordering::Relaxed);
112 if total == 0 {
113 return 0.0;
114 }
115 let current = self.current_item.load(Ordering::Relaxed);
116 (current as f64 / total as f64) * 100.0
117 }
118
119 #[must_use]
121 pub fn eta(&self) -> Option<Duration> {
122 let current = self.current_item.load(Ordering::Relaxed);
123 let total = self.total_items.load(Ordering::Relaxed);
124
125 if current == 0 || total == 0 || current >= total {
126 return None;
127 }
128
129 let elapsed = self.start_time.elapsed();
130 let per_item = elapsed.as_secs_f64() / current as f64;
131 let remaining_items = total - current;
132 let eta_seconds = per_item * remaining_items as f64;
133
134 Some(Duration::from_secs_f64(eta_seconds))
135 }
136
137 #[must_use]
139 pub fn info(&self) -> ProgressInfo {
140 ProgressInfo {
141 stage: *self.stage.read(),
142 current_item: self.current_item.load(Ordering::Relaxed),
143 total_items: self.total_items.load(Ordering::Relaxed),
144 percentage: self.percentage(),
145 eta_seconds: self.eta().map(|d| d.as_secs_f64()),
146 description: self.description.read().clone(),
147 }
148 }
149
150 pub fn set_bytes_processed(&self, bytes: u64) {
152 self.bytes_processed.store(bytes, Ordering::Relaxed);
153 }
154
155 pub fn add_bytes_processed(&self, bytes: u64) {
157 self.bytes_processed.fetch_add(bytes, Ordering::Relaxed);
158 }
159
160 pub fn set_total_bytes(&self, bytes: u64) {
162 self.total_bytes.store(bytes, Ordering::Relaxed);
163 }
164
165 #[must_use]
167 pub fn bytes_processed(&self) -> u64 {
168 self.bytes_processed.load(Ordering::Relaxed)
169 }
170
171 #[must_use]
173 pub fn total_bytes(&self) -> u64 {
174 self.total_bytes.load(Ordering::Relaxed)
175 }
176
177 #[must_use]
179 pub fn elapsed(&self) -> Duration {
180 self.start_time.elapsed()
181 }
182
183 pub fn reset(&mut self) {
185 *self.stage.write() = ProgressStage::LoadingTimeline;
186 self.current_item.store(0, Ordering::Relaxed);
187 self.total_items.store(0, Ordering::Relaxed);
188 self.bytes_processed.store(0, Ordering::Relaxed);
189 self.total_bytes.store(0, Ordering::Relaxed);
190 self.start_time = Instant::now();
191 self.description.write().clear();
192 }
193}
194
195impl Default for ProgressTracker {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201impl Clone for ProgressTracker {
202 fn clone(&self) -> Self {
203 Self {
204 stage: Arc::clone(&self.stage),
205 current_item: Arc::clone(&self.current_item),
206 total_items: Arc::clone(&self.total_items),
207 bytes_processed: Arc::clone(&self.bytes_processed),
208 total_bytes: Arc::clone(&self.total_bytes),
209 start_time: self.start_time,
210 description: Arc::clone(&self.description),
211 }
212 }
213}
214
215pub trait ProgressCallback: Send + Sync {
217 fn on_progress(&self, info: &ProgressInfo);
219}
220
221pub struct PrintProgressCallback;
223
224impl ProgressCallback for PrintProgressCallback {
225 fn on_progress(&self, info: &ProgressInfo) {
226 println!(
227 "[{:?}] {}/{} ({:.1}%) - {}",
228 info.stage, info.current_item, info.total_items, info.percentage, info.description
229 );
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_progress_tracker_creation() {
239 let tracker = ProgressTracker::new();
240 assert_eq!(tracker.stage(), ProgressStage::LoadingTimeline);
241 assert!((tracker.percentage() - 0.0).abs() < f64::EPSILON);
242 }
243
244 #[test]
245 fn test_progress_percentage() {
246 let tracker = ProgressTracker::new();
247 tracker.set_total_items(100);
248 tracker.set_current_item(50);
249 assert!((tracker.percentage() - 50.0).abs() < 0.01);
250 }
251
252 #[test]
253 fn test_increment_item() {
254 let tracker = ProgressTracker::new();
255 tracker.set_total_items(10);
256 tracker.increment_item();
257 tracker.increment_item();
258 assert_eq!(tracker.current_item.load(Ordering::Relaxed), 2);
259 }
260
261 #[test]
262 fn test_progress_info() {
263 let tracker = ProgressTracker::new();
264 tracker.set_total_items(100);
265 tracker.set_current_item(25);
266 tracker.set_description("Testing".to_string());
267
268 let info = tracker.info();
269 assert_eq!(info.current_item, 25);
270 assert_eq!(info.total_items, 100);
271 assert!((info.percentage - 25.0).abs() < 0.01);
272 assert_eq!(info.description, "Testing");
273 }
274
275 #[test]
276 fn test_eta() {
277 let tracker = ProgressTracker::new();
278 tracker.set_total_items(100);
279 tracker.set_current_item(0);
280 assert!(tracker.eta().is_none());
281
282 tracker.set_current_item(50);
283 std::thread::sleep(std::time::Duration::from_millis(10));
285 }
287}