use anyhow::{Context, Result};
use clap::Subcommand;
use colored::Colorize;
use std::path::PathBuf;
#[derive(Subcommand, Debug)]
pub enum EdlCommand {
Parse {
#[arg(value_name = "FILE")]
file: PathBuf,
#[arg(long, default_value = "cmx3600")]
format: String,
#[arg(long)]
strict: bool,
},
Validate {
#[arg(value_name = "FILE")]
file: PathBuf,
#[arg(long, default_value = "standard")]
level: String,
},
Export {
#[arg(value_name = "FILE")]
file: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(long, default_value = "cmx3600")]
format: String,
#[arg(long)]
comments: bool,
#[arg(long)]
clip_names: bool,
},
Conform {
#[arg(value_name = "FILE")]
file: PathBuf,
#[arg(long)]
media_dir: Option<PathBuf>,
},
}
pub async fn handle_edl_command(cmd: EdlCommand, json_output: bool) -> Result<()> {
match cmd {
EdlCommand::Parse {
file,
format,
strict,
} => parse_edl_file(&file, &format, strict, json_output).await,
EdlCommand::Validate { file, level } => validate_edl_file(&file, &level, json_output).await,
EdlCommand::Export {
file,
output,
format,
comments,
clip_names,
} => export_edl(&file, &output, &format, comments, clip_names, json_output).await,
EdlCommand::Conform { file, media_dir } => {
conform_edl(&file, media_dir.as_deref(), json_output).await
}
}
}
async fn parse_edl_file(
path: &PathBuf,
_format: &str,
strict: bool,
json_output: bool,
) -> Result<()> {
use oximedia_edl::EdlParser;
let text = std::fs::read_to_string(path)
.with_context(|| format!("Cannot read EDL file: {}", path.display()))?;
let mut parser = EdlParser::new();
parser.set_strict_mode(strict);
let edl = parser
.parse(&text)
.with_context(|| format!("Failed to parse EDL: {}", path.display()))?;
if json_output {
let events: Vec<serde_json::Value> = edl
.events
.iter()
.map(|e| {
serde_json::json!({
"number": e.number,
"reel": e.reel,
"clip_name": e.clip_name,
"comments": e.comments,
})
})
.collect();
let obj = serde_json::json!({
"file": path.to_string_lossy(),
"format": format!("{:?}", edl.format),
"title": edl.title,
"event_count": edl.event_count(),
"duration_seconds": edl.total_duration_seconds(),
"events": events,
});
println!("{}", serde_json::to_string_pretty(&obj)?);
return Ok(());
}
println!("{}", "EDL Parse".green().bold());
println!(" File: {}", path.display());
println!(" Format: {:?}", edl.format);
if let Some(ref title) = edl.title {
println!(" Title: {}", title);
}
println!(" Events: {}", edl.event_count());
println!(" Duration: {:.2}s", edl.total_duration_seconds());
if edl.events.is_empty() {
println!(" {}", "(no events found)".dimmed());
} else {
println!("\n {}", "Events:".cyan().bold());
let max_display = 20_usize;
for event in edl.events.iter().take(max_display) {
let clip = event.clip_name.as_deref().unwrap_or(&event.reel);
println!(" {:>4} {}", event.number.to_string().yellow(), clip);
if !event.comments.is_empty() {
for c in &event.comments {
println!(" {}", c.dimmed());
}
}
}
if edl.event_count() > max_display {
println!(
" {} ... and {} more",
"".dimmed(),
edl.event_count() - max_display
);
}
}
Ok(())
}
async fn validate_edl_file(path: &PathBuf, level: &str, json_output: bool) -> Result<()> {
use oximedia_edl::{EdlParser, EdlValidator};
let text = std::fs::read_to_string(path)
.with_context(|| format!("Cannot read EDL file: {}", path.display()))?;
let edl = EdlParser::new()
.parse(&text)
.with_context(|| format!("Failed to parse EDL: {}", path.display()))?;
let validator = match level.to_lowercase().as_str() {
"strict" => EdlValidator::strict(),
"lenient" => EdlValidator::lenient(),
_ => EdlValidator::standard(),
};
let report = validator
.validate(&edl)
.with_context(|| "EDL validation failed with an internal error")?;
if json_output {
let obj = serde_json::json!({
"file": path.to_string_lossy(),
"level": level,
"valid": !report.has_errors(),
"error_count": report.error_count(),
"warning_count": report.warning_count(),
"errors": report.errors,
"warnings": report.warnings,
});
println!("{}", serde_json::to_string_pretty(&obj)?);
return Ok(());
}
println!("{}", "EDL Validate".green().bold());
println!(" File: {}", path.display());
println!(" Level: {}", level.cyan());
if report.has_errors() {
println!(
" Status: {} ({} error(s))",
"INVALID".red().bold(),
report.error_count()
);
for err in &report.errors {
println!(" {} {}", "ERROR:".red(), err);
}
} else {
println!(" Status: {}", "VALID".green().bold());
}
if report.has_warnings() {
println!(" Warnings: {}", report.warning_count());
for warn in &report.warnings {
println!(" {} {}", "WARN:".yellow(), warn);
}
}
Ok(())
}
async fn export_edl(
path: &PathBuf,
output: &PathBuf,
_format: &str,
comments: bool,
clip_names: bool,
json_output: bool,
) -> Result<()> {
use oximedia_edl::{EdlGenerator, EdlParser};
let text = std::fs::read_to_string(path)
.with_context(|| format!("Cannot read EDL file: {}", path.display()))?;
let edl = EdlParser::new()
.parse(&text)
.with_context(|| format!("Failed to parse EDL: {}", path.display()))?;
let mut generator = EdlGenerator::new();
generator.set_include_comments(comments);
generator.set_include_clip_names(clip_names);
let out_text = generator
.generate(&edl)
.with_context(|| "Failed to generate EDL output")?;
std::fs::write(output, &out_text)
.with_context(|| format!("Failed to write EDL: {}", output.display()))?;
if json_output {
let obj = serde_json::json!({
"operation": "edl_export",
"input": path.to_string_lossy(),
"output": output.to_string_lossy(),
"events": edl.event_count(),
"bytes_written": out_text.len(),
});
println!("{}", serde_json::to_string_pretty(&obj)?);
return Ok(());
}
println!("{}", "EDL Export".green().bold());
println!(" Input: {}", path.display());
println!(" Output: {}", output.display());
println!(" Events: {}", edl.event_count());
println!(" Bytes: {}", out_text.len());
println!("{} Written: {}", "✓".green(), output.display());
Ok(())
}
async fn conform_edl(
path: &PathBuf,
media_dir: Option<&std::path::Path>,
json_output: bool,
) -> Result<()> {
use oximedia_edl::EdlParser;
let text = std::fs::read_to_string(path)
.with_context(|| format!("Cannot read EDL file: {}", path.display()))?;
let edl = EdlParser::new()
.parse(&text)
.with_context(|| format!("Failed to parse EDL: {}", path.display()))?;
let mut reels: Vec<String> = edl.events.iter().map(|e| e.reel.clone()).collect();
reels.sort();
reels.dedup();
if json_output {
let reel_statuses: Vec<serde_json::Value> = reels
.iter()
.map(|r| {
let found = media_dir.map_or(false, |d| {
d.join(r).exists()
|| d.join(format!("{}.mkv", r)).exists()
|| d.join(format!("{}.mov", r)).exists()
});
serde_json::json!({
"reel": r,
"status": if found { "online" } else { "offline" },
})
})
.collect();
let obj = serde_json::json!({
"file": path.to_string_lossy(),
"media_dir": media_dir.map(|d| d.to_string_lossy().into_owned()),
"event_count": edl.event_count(),
"unique_reels": reels.len(),
"reels": reel_statuses,
});
println!("{}", serde_json::to_string_pretty(&obj)?);
return Ok(());
}
println!("{}", "EDL Conform Report".green().bold());
println!(" EDL: {}", path.display());
if let Some(dir) = media_dir {
println!(" Media dir: {}", dir.display());
}
println!(" Events: {}", edl.event_count());
println!(" Reels: {}", reels.len());
let mut online = 0_usize;
let mut offline = 0_usize;
println!("\n {}", "Reel Status:".cyan().bold());
for reel in &reels {
let found = media_dir.map_or(false, |d| {
d.join(reel).exists()
|| d.join(format!("{}.mkv", reel)).exists()
|| d.join(format!("{}.mov", reel)).exists()
});
if found {
online += 1;
println!(" {} {}", "ONLINE ".green(), reel);
} else {
offline += 1;
let marker = if media_dir.is_some() {
"OFFLINE".red()
} else {
"UNKNOWN".yellow()
};
println!(" {} {}", marker, reel);
}
}
println!(
"\n Online: {} Offline/Unknown: {}",
online.to_string().green(),
offline.to_string().red()
);
Ok(())
}