use crate::config::ConformConfig;
use crate::error::ConformResult;
use crate::exporters::report::MatchReport;
use crate::session::ConformSession;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::{error, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchJob {
pub name: String,
pub timeline_path: PathBuf,
pub source_paths: Vec<PathBuf>,
pub output_path: PathBuf,
pub config: ConformConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchResult {
pub job_name: String,
pub success: bool,
pub report: Option<MatchReport>,
pub error: Option<String>,
pub duration_seconds: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchStatistics {
pub total_jobs: usize,
pub successful_jobs: usize,
pub failed_jobs: usize,
pub total_duration_seconds: f64,
pub avg_duration_seconds: f64,
}
pub struct BatchProcessor {
jobs: Vec<BatchJob>,
results: Arc<RwLock<Vec<BatchResult>>>,
parallel: bool,
max_parallel: usize,
}
impl BatchProcessor {
#[must_use]
pub fn new() -> Self {
Self {
jobs: Vec::new(),
results: Arc::new(RwLock::new(Vec::new())),
parallel: true,
max_parallel: num_cpus::get(),
}
}
pub fn add_job(&mut self, job: BatchJob) {
self.jobs.push(job);
}
pub fn add_jobs(&mut self, jobs: Vec<BatchJob>) {
self.jobs.extend(jobs);
}
pub fn set_parallel(&mut self, parallel: bool) {
self.parallel = parallel;
}
pub fn set_max_parallel(&mut self, max: usize) {
self.max_parallel = max;
}
pub async fn process(&mut self) -> ConformResult<BatchStatistics> {
info!("Starting batch processing of {} jobs", self.jobs.len());
let start_time = std::time::Instant::now();
#[cfg(not(target_arch = "wasm32"))]
if self.parallel {
self.process_parallel().await?;
} else {
self.process_sequential().await?;
}
#[cfg(target_arch = "wasm32")]
{
self.process_sequential().await?;
}
let duration = start_time.elapsed().as_secs_f64();
let stats = self.compute_statistics(duration);
info!(
"Batch processing complete: {}/{} successful in {:.2}s",
stats.successful_jobs, stats.total_jobs, duration
);
Ok(stats)
}
async fn process_sequential(&mut self) -> ConformResult<()> {
for job in &self.jobs {
let result = Self::process_job(job.clone()).await;
self.results.write().push(result);
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
async fn process_parallel(&mut self) -> ConformResult<()> {
let capacity = self.jobs.len();
let mut join_set = tokio::task::JoinSet::new();
for job in &self.jobs {
let job = job.clone();
join_set.spawn(async move { Self::process_job(job).await });
}
let mut results = Vec::with_capacity(capacity);
while let Some(outcome) = join_set.join_next().await {
match outcome {
Ok(result) => results.push(result),
Err(join_err) => {
error!("parallel batch job task panicked: {join_err}");
results.push(BatchResult {
job_name: String::new(),
success: false,
report: None,
error: Some(format!("task panicked: {join_err}")),
duration_seconds: 0.0,
});
}
}
}
*self.results.write() = results;
Ok(())
}
async fn process_job(job: BatchJob) -> BatchResult {
info!("Processing job: {}", job.name);
let start_time = std::time::Instant::now();
match ConformSession::new(
job.name.clone(),
&job.timeline_path,
job.source_paths.clone(),
job.output_path.clone(),
job.config.clone(),
) {
Ok(mut session) => match session.run().await {
Ok(report) => {
info!("Job {} completed successfully", job.name);
BatchResult {
job_name: job.name,
success: true,
report: Some(report),
error: None,
duration_seconds: start_time.elapsed().as_secs_f64(),
}
}
Err(e) => {
error!("Job {} failed: {}", job.name, e);
BatchResult {
job_name: job.name,
success: false,
report: None,
error: Some(e.to_string()),
duration_seconds: start_time.elapsed().as_secs_f64(),
}
}
},
Err(e) => {
error!("Failed to create session for job {}: {}", job.name, e);
BatchResult {
job_name: job.name,
success: false,
report: None,
error: Some(e.to_string()),
duration_seconds: start_time.elapsed().as_secs_f64(),
}
}
}
}
fn compute_statistics(&self, total_duration: f64) -> BatchStatistics {
let results = self.results.read();
let total_jobs = results.len();
let successful_jobs = results.iter().filter(|r| r.success).count();
let failed_jobs = total_jobs - successful_jobs;
let avg_duration = if total_jobs > 0 {
total_duration / total_jobs as f64
} else {
0.0
};
BatchStatistics {
total_jobs,
successful_jobs,
failed_jobs,
total_duration_seconds: total_duration,
avg_duration_seconds: avg_duration,
}
}
#[must_use]
pub fn get_results(&self) -> Vec<BatchResult> {
self.results.read().clone()
}
#[must_use]
pub fn get_successful_results(&self) -> Vec<BatchResult> {
self.results
.read()
.iter()
.filter(|r| r.success)
.cloned()
.collect()
}
#[must_use]
pub fn get_failed_results(&self) -> Vec<BatchResult> {
self.results
.read()
.iter()
.filter(|r| !r.success)
.cloned()
.collect()
}
pub fn export_results_json(&self) -> ConformResult<String> {
let results = self.results.read();
Ok(serde_json::to_string_pretty(&*results)?)
}
pub fn save_results<P: AsRef<Path>>(&self, path: P) -> ConformResult<()> {
let json = self.export_results_json()?;
std::fs::write(path, json)?;
Ok(())
}
pub fn load_jobs<P: AsRef<Path>>(path: P) -> ConformResult<Vec<BatchJob>> {
let content = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&content)?)
}
pub fn save_jobs<P: AsRef<Path>>(jobs: &[BatchJob], path: P) -> ConformResult<()> {
let json = serde_json::to_string_pretty(jobs)?;
std::fs::write(path, json)?;
Ok(())
}
}
impl Default for BatchProcessor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_batch_processor_creation() {
let processor = BatchProcessor::new();
assert_eq!(processor.jobs.len(), 0);
assert!(processor.parallel);
}
#[test]
fn test_add_job() {
let mut processor = BatchProcessor::new();
let job = BatchJob {
name: "Test Job".to_string(),
timeline_path: PathBuf::from("/test/timeline.edl"),
source_paths: vec![PathBuf::from("/test/media")],
output_path: PathBuf::from("/test/output"),
config: ConformConfig::default(),
};
processor.add_job(job);
assert_eq!(processor.jobs.len(), 1);
}
#[test]
fn test_statistics_computation() {
let processor = BatchProcessor::new();
let stats = processor.compute_statistics(100.0);
assert_eq!(stats.total_jobs, 0);
assert_eq!(stats.successful_jobs, 0);
}
}