1pub mod aaf_edl;
56pub mod ale;
57pub mod cmx3600;
58pub mod converter;
59pub mod fcpxml;
60pub mod otio;
61pub mod validator;
62
63use oximedia_core::Rational;
64use std::collections::HashMap;
65use thiserror::Error;
66
67#[derive(Debug, Error)]
69pub enum EdlError {
70 #[error("Parse error at line {line}: {message}")]
72 ParseError {
73 line: usize,
75 message: String,
77 },
78
79 #[error("Invalid timecode: {0}")]
81 InvalidTimecode(String),
82
83 #[error("Invalid edit number: {0}")]
85 InvalidEditNumber(u32),
86
87 #[error("Unsupported feature: {0}")]
89 UnsupportedFeature(String),
90
91 #[error("Conversion error: {0}")]
93 ConversionError(String),
94
95 #[error("Validation error: {0}")]
97 ValidationError(String),
98
99 #[error("IO error: {0}")]
101 IoError(#[from] std::io::Error),
102
103 #[error("XML error: {0}")]
105 XmlError(String),
106
107 #[error("JSON error: {0}")]
109 JsonError(String),
110
111 #[error("Missing required field: {0}")]
113 MissingField(String),
114
115 #[error("Invalid format: {0}")]
117 InvalidFormat(String),
118}
119
120pub type EdlResult<T> = Result<T, EdlError>;
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub enum EdlFormat {
126 Cmx3600,
128 Ale,
130 FcpXml,
132 AafEdl,
134 Otio,
136}
137
138#[derive(Debug, Clone, PartialEq)]
140pub struct Timecode {
141 pub hours: u8,
143 pub minutes: u8,
145 pub seconds: u8,
147 pub frames: u8,
149 pub drop_frame: bool,
151 pub frame_rate: Rational,
153}
154
155impl Timecode {
156 #[must_use]
158 pub fn new(
159 hours: u8,
160 minutes: u8,
161 seconds: u8,
162 frames: u8,
163 drop_frame: bool,
164 frame_rate: Rational,
165 ) -> Self {
166 Self {
167 hours,
168 minutes,
169 seconds,
170 frames,
171 drop_frame,
172 frame_rate,
173 }
174 }
175
176 #[must_use]
178 pub fn from_frames(frames: i64, frame_rate: Rational, drop_frame: bool) -> Self {
179 let fps = frame_rate.to_f64() as i64;
180 let frames_per_minute = fps * 60;
181 let frames_per_hour = frames_per_minute * 60;
182
183 let hours = (frames / frames_per_hour) as u8;
184 let remaining = frames % frames_per_hour;
185 let minutes = (remaining / frames_per_minute) as u8;
186 let remaining = remaining % frames_per_minute;
187 let seconds = (remaining / fps) as u8;
188 let frames = (remaining % fps) as u8;
189
190 Self::new(hours, minutes, seconds, frames, drop_frame, frame_rate)
191 }
192
193 #[must_use]
195 pub fn to_frames(&self) -> i64 {
196 let fps = self.frame_rate.to_f64() as i64;
197 let total_seconds =
198 i64::from(self.hours) * 3600 + i64::from(self.minutes) * 60 + i64::from(self.seconds);
199 total_seconds * fps + i64::from(self.frames)
200 }
201
202 pub fn parse(s: &str, frame_rate: Rational) -> EdlResult<Self> {
204 let parts: Vec<&str> = s.split(&[':', ';'][..]).collect();
205 if parts.len() != 4 {
206 return Err(EdlError::InvalidTimecode(s.to_string()));
207 }
208
209 let hours = parts[0]
210 .parse()
211 .map_err(|_| EdlError::InvalidTimecode(s.to_string()))?;
212 let minutes = parts[1]
213 .parse()
214 .map_err(|_| EdlError::InvalidTimecode(s.to_string()))?;
215 let seconds = parts[2]
216 .parse()
217 .map_err(|_| EdlError::InvalidTimecode(s.to_string()))?;
218 let frames = parts[3]
219 .parse()
220 .map_err(|_| EdlError::InvalidTimecode(s.to_string()))?;
221 let drop_frame = s.contains(';');
222
223 Ok(Self::new(
224 hours, minutes, seconds, frames, drop_frame, frame_rate,
225 ))
226 }
227
228 #[must_use]
230 pub fn format(&self) -> String {
231 let separator = if self.drop_frame { ';' } else { ':' };
232 format!(
233 "{:02}:{:02}:{:02}{}{:02}",
234 self.hours, self.minutes, self.seconds, separator, self.frames
235 )
236 }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum EditType {
242 Cut,
244 Dissolve,
246 Wipe,
248 Key,
250}
251
252#[derive(Debug, Clone)]
254pub struct EdlEvent {
255 pub number: u32,
257 pub reel: String,
259 pub track: String,
261 pub edit_type: EditType,
263 pub source_in: Timecode,
265 pub source_out: Timecode,
267 pub record_in: Timecode,
269 pub record_out: Timecode,
271 pub transition_duration: Option<u32>,
273 pub motion_effect: Option<MotionEffect>,
275 pub comments: Vec<String>,
277 pub metadata: HashMap<String, String>,
279}
280
281#[derive(Debug, Clone)]
283pub struct MotionEffect {
284 pub speed: f64,
286 pub freeze: bool,
288 pub reverse: bool,
290 pub entry: Option<Timecode>,
292}
293
294#[derive(Debug, Clone)]
296pub struct Edl {
297 pub title: String,
299 pub frame_rate: Rational,
301 pub drop_frame: bool,
303 pub events: Vec<EdlEvent>,
305 pub comments: Vec<String>,
307 pub metadata: HashMap<String, String>,
309}
310
311impl Edl {
312 #[must_use]
314 pub fn new(title: String, frame_rate: Rational, drop_frame: bool) -> Self {
315 Self {
316 title,
317 frame_rate,
318 drop_frame,
319 events: Vec::new(),
320 comments: Vec::new(),
321 metadata: HashMap::new(),
322 }
323 }
324
325 pub fn parse(content: &str, format: EdlFormat) -> EdlResult<Self> {
327 match format {
328 EdlFormat::Cmx3600 => cmx3600::parse(content),
329 EdlFormat::Ale => ale::parse(content),
330 EdlFormat::FcpXml => fcpxml::parse(content),
331 EdlFormat::AafEdl => aaf_edl::parse(content),
332 EdlFormat::Otio => otio::parse(content),
333 }
334 }
335
336 pub fn to_format(&self, format: EdlFormat) -> EdlResult<String> {
338 match format {
339 EdlFormat::Cmx3600 => cmx3600::write(self),
340 EdlFormat::Ale => ale::write(self),
341 EdlFormat::FcpXml => fcpxml::write(self),
342 EdlFormat::AafEdl => aaf_edl::write(self),
343 EdlFormat::Otio => otio::write(self),
344 }
345 }
346
347 pub fn add_event(&mut self, event: EdlEvent) {
349 self.events.push(event);
350 }
351
352 #[must_use]
354 pub fn get_event(&self, number: u32) -> Option<&EdlEvent> {
355 self.events.iter().find(|e| e.number == number)
356 }
357
358 pub fn sort_events(&mut self) {
360 self.events.sort_by_key(|e| e.record_in.to_frames());
361 }
362
363 pub fn validate(&self) -> EdlResult<validator::ValidationReport> {
365 validator::EdlValidator::new().validate(self)
366 }
367}