Skip to main content

oximedia_conform/
progress.rs

1//! Progress tracking for conform operations.
2
3use serde::{Deserialize, Serialize};
4use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8/// Progress stage.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum ProgressStage {
11    /// Loading timeline.
12    LoadingTimeline,
13    /// Scanning media.
14    ScanningMedia,
15    /// Matching clips.
16    MatchingClips,
17    /// Validating matches.
18    Validating,
19    /// Conforming timeline.
20    Conforming,
21    /// Exporting output.
22    Exporting,
23    /// Complete.
24    Complete,
25}
26
27/// Progress information.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ProgressInfo {
30    /// Current stage.
31    pub stage: ProgressStage,
32    /// Current item being processed.
33    pub current_item: usize,
34    /// Total items to process.
35    pub total_items: usize,
36    /// Progress percentage (0-100).
37    pub percentage: f64,
38    /// Estimated time remaining in seconds.
39    pub eta_seconds: Option<f64>,
40    /// Current operation description.
41    pub description: String,
42}
43
44/// Progress tracker for conform operations.
45pub struct ProgressTracker {
46    /// Current stage.
47    stage: Arc<parking_lot::RwLock<ProgressStage>>,
48    /// Current item.
49    current_item: Arc<AtomicUsize>,
50    /// Total items.
51    total_items: Arc<AtomicUsize>,
52    /// Bytes processed.
53    bytes_processed: Arc<AtomicU64>,
54    /// Total bytes.
55    total_bytes: Arc<AtomicU64>,
56    /// Start time.
57    start_time: Instant,
58    /// Current operation description.
59    description: Arc<parking_lot::RwLock<String>>,
60}
61
62impl ProgressTracker {
63    /// Create a new progress tracker.
64    #[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    /// Set the current stage.
78    pub fn set_stage(&self, stage: ProgressStage) {
79        *self.stage.write() = stage;
80    }
81
82    /// Get the current stage.
83    #[must_use]
84    pub fn stage(&self) -> ProgressStage {
85        *self.stage.read()
86    }
87
88    /// Set total items.
89    pub fn set_total_items(&self, total: usize) {
90        self.total_items.store(total, Ordering::Relaxed);
91    }
92
93    /// Set current item.
94    pub fn set_current_item(&self, current: usize) {
95        self.current_item.store(current, Ordering::Relaxed);
96    }
97
98    /// Increment current item.
99    pub fn increment_item(&self) {
100        self.current_item.fetch_add(1, Ordering::Relaxed);
101    }
102
103    /// Set description.
104    pub fn set_description(&self, desc: String) {
105        *self.description.write() = desc;
106    }
107
108    /// Get current progress percentage.
109    #[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    /// Get estimated time remaining.
120    #[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    /// Get current progress information.
138    #[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    /// Set bytes processed.
151    pub fn set_bytes_processed(&self, bytes: u64) {
152        self.bytes_processed.store(bytes, Ordering::Relaxed);
153    }
154
155    /// Add bytes processed.
156    pub fn add_bytes_processed(&self, bytes: u64) {
157        self.bytes_processed.fetch_add(bytes, Ordering::Relaxed);
158    }
159
160    /// Set total bytes.
161    pub fn set_total_bytes(&self, bytes: u64) {
162        self.total_bytes.store(bytes, Ordering::Relaxed);
163    }
164
165    /// Get bytes processed.
166    #[must_use]
167    pub fn bytes_processed(&self) -> u64 {
168        self.bytes_processed.load(Ordering::Relaxed)
169    }
170
171    /// Get total bytes.
172    #[must_use]
173    pub fn total_bytes(&self) -> u64 {
174        self.total_bytes.load(Ordering::Relaxed)
175    }
176
177    /// Get elapsed time.
178    #[must_use]
179    pub fn elapsed(&self) -> Duration {
180        self.start_time.elapsed()
181    }
182
183    /// Reset the tracker.
184    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
215/// Callback trait for progress updates.
216pub trait ProgressCallback: Send + Sync {
217    /// Called when progress is updated.
218    fn on_progress(&self, info: &ProgressInfo);
219}
220
221/// Simple progress callback that prints to stdout.
222pub 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        // ETA should be available when in progress
284        std::thread::sleep(std::time::Duration::from_millis(10));
285        // Cannot reliably test exact ETA due to timing
286    }
287}