Skip to main content

oximedia_proxy/generate/
batch.rs

1//! Batch proxy generation for processing multiple files.
2
3use super::{encoder::ProxyEncodeResult, settings::ProxyGenerationSettings, ProxyEncoder};
4use crate::Result;
5use rayon::prelude::*;
6use std::path::{Path, PathBuf};
7
8/// Batch proxy generator for processing multiple files in parallel.
9pub struct BatchProxyGenerator {
10    settings: ProxyGenerationSettings,
11    max_parallel: usize,
12}
13
14impl BatchProxyGenerator {
15    /// Create a new batch proxy generator.
16    pub fn new(settings: ProxyGenerationSettings) -> Self {
17        Self {
18            settings,
19            max_parallel: num_cpus::get(),
20        }
21    }
22
23    /// Set the maximum number of parallel encodes.
24    #[must_use]
25    pub fn with_max_parallel(mut self, max_parallel: usize) -> Self {
26        self.max_parallel = max_parallel.max(1);
27        self
28    }
29
30    /// Generate proxies for multiple input files.
31    ///
32    /// # Errors
33    ///
34    /// Returns an error if any encoding operation fails.
35    pub async fn generate_batch(&self, inputs: &[(PathBuf, PathBuf)]) -> Result<Vec<BatchResult>> {
36        tracing::info!("Starting batch generation for {} files", inputs.len());
37
38        // Process in parallel using rayon
39        let settings = self.settings.clone();
40        let results: Vec<BatchResult> = inputs
41            .par_iter()
42            .map(|(input, output)| {
43                let encoder = ProxyEncoder::new(settings.clone())?;
44                let result =
45                    tokio::runtime::Handle::current().block_on(encoder.encode(input, output));
46
47                match result {
48                    Ok(encode_result) => Ok(BatchResult::Success {
49                        input: input.clone(),
50                        output: output.clone(),
51                        result: encode_result,
52                    }),
53                    Err(e) => Ok(BatchResult::Failed {
54                        input: input.clone(),
55                        output: output.clone(),
56                        error: e.to_string(),
57                    }),
58                }
59            })
60            .collect::<Result<Vec<_>>>()?;
61
62        let success_count = results
63            .iter()
64            .filter(|r| matches!(r, BatchResult::Success { .. }))
65            .count();
66        let failed_count = results.len() - success_count;
67
68        tracing::info!(
69            "Batch generation complete: {} succeeded, {} failed",
70            success_count,
71            failed_count
72        );
73
74        Ok(results)
75    }
76
77    /// Generate proxies with a callback for progress tracking.
78    pub async fn generate_batch_with_progress<F>(
79        &self,
80        inputs: &[(PathBuf, PathBuf)],
81        mut progress_callback: F,
82    ) -> Result<Vec<BatchResult>>
83    where
84        F: FnMut(usize, usize) + Send,
85    {
86        let total = inputs.len();
87        let mut completed = 0;
88
89        let settings = self.settings.clone();
90        let mut results = Vec::with_capacity(total);
91
92        for (input, output) in inputs {
93            let encoder = ProxyEncoder::new(settings.clone())?;
94            let result = encoder.encode(input, output).await;
95
96            let batch_result = match result {
97                Ok(encode_result) => BatchResult::Success {
98                    input: input.clone(),
99                    output: output.clone(),
100                    result: encode_result,
101                },
102                Err(e) => BatchResult::Failed {
103                    input: input.clone(),
104                    output: output.clone(),
105                    error: e.to_string(),
106                },
107            };
108
109            results.push(batch_result);
110            completed += 1;
111            progress_callback(completed, total);
112        }
113
114        Ok(results)
115    }
116}
117
118/// Result of a batch proxy generation operation.
119#[derive(Debug, Clone)]
120pub enum BatchResult {
121    /// Successful generation.
122    Success {
123        /// Input file path.
124        input: PathBuf,
125        /// Output file path.
126        output: PathBuf,
127        /// Encoding result.
128        result: ProxyEncodeResult,
129    },
130    /// Failed generation.
131    Failed {
132        /// Input file path.
133        input: PathBuf,
134        /// Output file path.
135        output: PathBuf,
136        /// Error message.
137        error: String,
138    },
139}
140
141impl BatchResult {
142    /// Check if this result is successful.
143    #[must_use]
144    pub const fn is_success(&self) -> bool {
145        matches!(self, Self::Success { .. })
146    }
147
148    /// Check if this result is a failure.
149    #[must_use]
150    pub const fn is_failed(&self) -> bool {
151        matches!(self, Self::Failed { .. })
152    }
153
154    /// Get the input path.
155    #[must_use]
156    pub fn input(&self) -> &Path {
157        match self {
158            Self::Success { input, .. } | Self::Failed { input, .. } => input,
159        }
160    }
161
162    /// Get the output path.
163    #[must_use]
164    pub fn output(&self) -> &Path {
165        match self {
166            Self::Success { output, .. } | Self::Failed { output, .. } => output,
167        }
168    }
169}
170
171/// Helper function to get the number of CPUs.
172#[allow(dead_code)]
173fn num_cpus() -> usize {
174    std::thread::available_parallelism()
175        .map(|n| n.get())
176        .unwrap_or(1)
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_batch_generator_creation() {
185        let settings = ProxyGenerationSettings::quarter_res_h264();
186        let generator = BatchProxyGenerator::new(settings);
187        assert!(generator.max_parallel > 0);
188    }
189
190    #[test]
191    fn test_max_parallel() {
192        let settings = ProxyGenerationSettings::quarter_res_h264();
193        let generator = BatchProxyGenerator::new(settings).with_max_parallel(4);
194        assert_eq!(generator.max_parallel, 4);
195    }
196
197    #[test]
198    fn test_batch_result() {
199        let result = BatchResult::Failed {
200            input: PathBuf::from("input.mov"),
201            output: PathBuf::from("output.mp4"),
202            error: "test error".to_string(),
203        };
204
205        assert!(result.is_failed());
206        assert!(!result.is_success());
207        assert_eq!(result.input(), Path::new("input.mov"));
208        assert_eq!(result.output(), Path::new("output.mp4"));
209    }
210}