pub mod capcut;
pub mod cdl;
mod common;
pub mod edit_yaml;
pub mod edl;
pub mod export;
pub mod fcp7;
pub mod fcpxml;
pub mod mlt;
pub mod resolve;
mod xml;
use std::collections::BTreeSet;
use std::fmt;
use std::path::Path;
use std::str::FromStr;
use ffcine_core::{Error, Result};
use ffcine_timeline::Timeline;
pub type BeatsFn = fn(&Path) -> Option<Vec<f64>>;
#[derive(Debug, Clone, Copy, Default)]
pub struct ImportOptions {
pub frame_size: Option<(u32, u32)>,
pub rate: Option<f64>,
pub media_duration: Option<fn(&Path) -> Option<f64>>,
pub beats: Option<BeatsFn>,
pub strict: bool,
}
#[derive(Debug, Clone)]
pub struct Imported {
pub timeline: Timeline,
pub report: ImportReport,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ImportReport {
pub warnings: Vec<String>,
pub unmapped: Vec<String>,
pub skipped: Vec<String>,
}
impl ImportReport {
pub fn warn(&mut self, msg: impl Into<String>) {
let msg = msg.into();
if !self.warnings.contains(&msg) {
self.warnings.push(msg);
}
}
pub fn skip(&mut self, msg: impl Into<String>) {
let msg = msg.into();
if !self.skipped.contains(&msg) {
self.skipped.push(msg.clone());
}
self.warn(format!("skipped: {msg}"));
}
pub fn unmapped<I: IntoIterator<Item = String>>(&mut self, paths: I) {
let mut all: BTreeSet<String> = self.unmapped.drain(..).collect();
all.extend(paths);
self.unmapped = all.into_iter().collect();
}
}
impl fmt::Display for ImportReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for w in &self.warnings {
writeln!(f, "warning: {w}")?;
}
if !self.unmapped.is_empty() {
writeln!(f, "kept under ffcine.foreign: {}", self.unmapped.join(", "))?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Format {
EditYaml,
Otio,
Fcpxml,
Mlt,
Resolve,
Fcp7,
Edl,
CapCut,
}
impl Format {
pub fn detect(path: &Path) -> Option<Self> {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default()
.to_ascii_lowercase();
match ext.as_str() {
"yaml" | "yml" => Some(Format::EditYaml),
"otio" => Some(Format::Otio),
"fcpxml" | "fcpxmld" => Some(Format::Fcpxml),
"mlt" | "kdenlive" => Some(Format::Mlt),
"edl" => Some(Format::Edl),
"xml" => {
let head = sniff(path)?;
if head.contains("<xmeml") {
Some(Format::Fcp7)
} else if head.contains("<fcpxml") {
Some(Format::Fcpxml)
} else if head.contains("<mlt") {
Some(Format::Mlt)
} else {
None
}
}
"json" => {
let head = sniff(path)?;
(head.contains("\"materials\"") || head.contains("\"canvas_config\""))
.then_some(Format::CapCut)
}
_ if path.is_dir() => capcut::draft_file(path).map(|_| Format::CapCut),
_ => None,
}
}
}
impl FromStr for Format {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"edit-yaml" | "edit_yaml" | "yaml" => Ok(Format::EditYaml),
"otio" => Ok(Format::Otio),
"fcpxml" | "fcpxmld" | "fcpx" => Ok(Format::Fcpxml),
"mlt" | "kdenlive" | "shotcut" => Ok(Format::Mlt),
"resolve" => Ok(Format::Resolve),
"fcp7" | "xmeml" | "premiere" | "fcp-xml" | "fcp_xml" => Ok(Format::Fcp7),
"edl" | "cmx3600" | "cmx_3600" => Ok(Format::Edl),
"capcut" | "jianying" => Ok(Format::CapCut),
_ => Err(Error::Invalid {
what: "import format",
value: s.to_owned(),
}),
}
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Format::EditYaml => "edit-yaml",
Format::Otio => "otio",
Format::Fcpxml => "fcpxml",
Format::Mlt => "mlt",
Format::Resolve => "resolve",
Format::Fcp7 => "fcp7",
Format::Edl => "edl",
Format::CapCut => "capcut",
})
}
}
pub fn import(input: &Path, format: Option<Format>, opts: &ImportOptions) -> Result<Imported> {
let format = format
.or_else(|| Format::detect(input))
.ok_or_else(|| Error::Invalid {
what: "import format (pass one)",
value: input.display().to_string(),
})?;
let imported = match format {
Format::EditYaml => edit_yaml::import(input, opts),
Format::Fcpxml => fcpxml::import(input, opts),
Format::Mlt => mlt::import(input, opts),
Format::Resolve => resolve::import(input, opts),
Format::Fcp7 => fcp7::import(input, opts),
Format::Edl => edl::import(input, opts),
Format::CapCut => capcut::import(input, opts),
Format::Otio if resolve::detect(input) => resolve::import(input, opts),
Format::Otio => Ok(Imported {
timeline: Timeline::from_file(input)?,
report: ImportReport::default(),
}),
}?;
if opts.strict && !imported.report.skipped.is_empty() {
return Err(Error::Invalid {
what: "input (strict import)",
value: imported.report.skipped.join("; "),
});
}
Ok(imported)
}
fn sniff(path: &Path) -> Option<String> {
use std::io::Read;
let mut buf = vec![0; 4096];
let n = std::fs::File::open(path).ok()?.read(&mut buf).ok()?;
buf.truncate(n);
Some(String::from_utf8_lossy(&buf).into_owned())
}