use clap::Parser;
use mp4box::edit::{Command, Editor};
#[derive(Parser, Debug)]
#[command(version, about = "Non-destructive MP4/ISOBMFF box editor")]
struct Args {
#[arg(long = "remove", value_name = "PATH")]
remove: Vec<String>,
#[arg(long = "remove-all", value_name = "FOURCC")]
remove_all: Vec<String>,
#[arg(long = "insert", value_name = "PATH:FILE[:POS]")]
insert: Vec<String>,
#[arg(long = "replace", value_name = "PATH:FILE")]
replace: Vec<String>,
#[arg(long = "set", value_name = "PATH.FIELD=VALUE")]
set: Vec<String>,
#[arg(long = "tag", value_name = "NAME=VALUE")]
tag: Vec<String>,
#[arg(long = "faststart")]
faststart: bool,
input: String,
output: String,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let mut editor = Editor::new();
for path in &args.remove {
editor.add_command(Command::Remove { path: path.clone() });
}
for fourcc in &args.remove_all {
editor.add_command(Command::RemoveAll {
fourcc: fourcc.clone(),
});
}
for spec in &args.insert {
let parts: Vec<&str> = spec.splitn(3, ':').collect();
anyhow::ensure!(
parts.len() >= 2,
"--insert format is PATH:FILE or PATH:FILE:POSITION, got '{}'",
spec
);
let position =
match parts.get(2) {
Some(p) => Some(p.parse::<usize>().map_err(|_| {
anyhow::anyhow!("--insert position must be a number, got '{}'", p)
})?),
None => None,
};
editor.add_command(Command::Insert {
parent: parts[0].to_string(),
bytes: std::fs::read(parts[1])?,
position,
});
}
for spec in &args.replace {
let (path, file) = spec
.split_once(':')
.ok_or_else(|| anyhow::anyhow!("--replace format is PATH:FILE, got '{}'", spec))?;
editor.add_command(Command::Replace {
path: path.to_string(),
bytes: std::fs::read(file)?,
});
}
for spec in &args.set {
let (lhs, value) = spec
.split_once('=')
.ok_or_else(|| anyhow::anyhow!("--set format is PATH.FIELD=VALUE, got '{}'", spec))?;
let (path, field) = lhs
.rsplit_once('.')
.ok_or_else(|| anyhow::anyhow!("--set format is PATH.FIELD=VALUE, got '{}'", spec))?;
editor.add_command(Command::Set {
path: path.to_string(),
field: field.to_string(),
value: value.to_string(),
});
}
for spec in &args.tag {
let (name, value) = spec
.split_once('=')
.ok_or_else(|| anyhow::anyhow!("--tag format is NAME=VALUE, got '{}'", spec))?;
editor.set_tag(name, value)?;
}
if args.faststart {
editor.faststart();
}
let stats = editor.process_file(&args.input, &args.output)?;
println!(
"wrote {} ({} bytes, {} chunk offsets adjusted{})",
args.output,
stats.bytes_written,
stats.chunk_offsets_adjusted,
if stats.chunk_offsets_unmapped > 0 {
format!(", {} unmapped!", stats.chunk_offsets_unmapped)
} else {
String::new()
}
);
Ok(())
}