1#![warn(missing_docs)]
88#![allow(
89 clippy::module_name_repetitions,
90 clippy::cast_possible_truncation,
91 clippy::cast_precision_loss,
92 clippy::cast_sign_loss,
93 dead_code,
94 clippy::pedantic
95)]
96
97pub mod aaf;
98pub mod ale;
99pub mod audio;
100pub mod batch_export;
101pub mod cmx3600;
102pub mod conform_report;
103pub mod consolidate;
104pub mod converter;
105pub mod diff;
106pub mod edl_ascii_timeline;
107pub mod edl_changelist;
108pub mod edl_comments;
109pub mod edl_compare;
110pub mod edl_duration;
111pub mod edl_event;
112pub mod edl_filter;
113pub mod edl_merge;
114pub mod edl_sanitize;
115pub mod edl_statistics;
116pub mod edl_timeline;
117pub mod edl_validator;
118pub mod error;
119pub mod event;
120pub mod event_list;
121pub mod fcpxml;
122pub mod filter;
123pub mod frame_count;
124pub mod fuzz_tests;
125pub mod generator;
126pub mod lazy_parser;
127pub mod metadata;
128pub mod motion;
129pub mod multicam;
130pub mod optimizer;
131pub mod otio;
132pub mod parser;
133pub mod parser_opt;
134pub mod reel;
135pub mod reel_map;
136pub mod reel_registry;
137pub mod roundtrip;
138pub mod subframe;
139pub mod tc_cache;
140pub mod tc_list;
141pub mod timecode;
142pub mod to_timeline;
143pub mod transition_events;
144pub mod validator;
145pub mod version_history;
146
147pub use ale::{parse_ale_records, AleRecord};
148pub use batch_export::BatchEdlExporter;
149pub use error::{EdlError, EdlResult};
150pub use generator::EdlGenerator;
151pub use parser::{parse_edl, EdlParser};
152pub use validator::EdlValidator;
153
154use crate::event::EdlEvent;
155use crate::reel::ReelTable;
156use crate::timecode::EdlFrameRate;
157use std::path::PathBuf;
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
161pub enum EdlFormat {
162 Cmx3600,
164 Cmx3400,
166 Cmx340,
168 Gvg,
170 SonyBve9000,
172}
173
174impl EdlFormat {
175 #[must_use]
177 pub const fn as_str(&self) -> &'static str {
178 match self {
179 Self::Cmx3600 => "CMX 3600",
180 Self::Cmx3400 => "CMX 3400",
181 Self::Cmx340 => "CMX 340",
182 Self::Gvg => "GVG",
183 Self::SonyBve9000 => "Sony BVE-9000",
184 }
185 }
186}
187
188impl std::fmt::Display for EdlFormat {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 write!(f, "{}", self.as_str())
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct Edl {
197 pub format: EdlFormat,
199
200 pub title: Option<String>,
202
203 pub frame_rate: EdlFrameRate,
205
206 pub events: Vec<EdlEvent>,
208
209 pub reel_table: ReelTable,
211
212 pub source_file: Option<PathBuf>,
214
215 pub global_comments: Vec<String>,
217}
218
219impl Edl {
220 #[must_use]
222 pub fn new(format: EdlFormat) -> Self {
223 Self {
224 format,
225 title: None,
226 frame_rate: EdlFrameRate::Fps2997NDF,
227 events: Vec::new(),
228 reel_table: ReelTable::new(),
229 source_file: None,
230 global_comments: Vec::new(),
231 }
232 }
233
234 #[must_use]
236 pub fn cmx3600() -> Self {
237 Self::new(EdlFormat::Cmx3600)
238 }
239
240 pub fn set_title(&mut self, title: String) {
242 self.title = Some(title);
243 }
244
245 pub fn set_frame_rate(&mut self, frame_rate: EdlFrameRate) {
247 self.frame_rate = frame_rate;
248 }
249
250 pub fn set_source_file(&mut self, path: PathBuf) {
252 self.source_file = Some(path);
253 }
254
255 pub fn add_event(&mut self, event: EdlEvent) -> EdlResult<()> {
261 event.validate()?;
262 self.events.push(event);
263 Ok(())
264 }
265
266 pub fn add_global_comment(&mut self, comment: String) {
268 self.global_comments.push(comment);
269 }
270
271 #[must_use]
273 pub fn get_event(&self, number: u32) -> Option<&EdlEvent> {
274 self.events.iter().find(|e| e.number == number)
275 }
276
277 pub fn get_event_mut(&mut self, number: u32) -> Option<&mut EdlEvent> {
279 self.events.iter_mut().find(|e| e.number == number)
280 }
281
282 pub fn remove_event(&mut self, number: u32) -> Option<EdlEvent> {
284 if let Some(index) = self.events.iter().position(|e| e.number == number) {
285 Some(self.events.remove(index))
286 } else {
287 None
288 }
289 }
290
291 #[must_use]
293 pub fn event_count(&self) -> usize {
294 self.events.len()
295 }
296
297 #[must_use]
299 pub fn total_duration_frames(&self) -> u64 {
300 self.events.iter().map(|e| e.duration_frames()).sum()
301 }
302
303 #[must_use]
305 pub fn total_duration_seconds(&self) -> f64 {
306 self.total_duration_frames() as f64 / self.frame_rate.fps() as f64
307 }
308
309 pub fn sort_events(&mut self) {
311 self.events.sort_by_key(|e| e.record_in.to_frames());
312 }
313
314 pub fn renumber_events(&mut self) {
316 for (i, event) in self.events.iter_mut().enumerate() {
317 event.number = (i + 1) as u32;
318 }
319 }
320
321 pub fn validate(&self) -> EdlResult<()> {
327 let validator = EdlValidator::default();
328 validator.validate(self)?;
329 Ok(())
330 }
331
332 pub fn to_string_format(&self) -> EdlResult<String> {
338 let generator = EdlGenerator::new();
339 generator.generate(self)
340 }
341
342 #[allow(clippy::should_implement_trait)]
348 pub fn from_str(input: &str) -> EdlResult<Self> {
349 parse_edl(input)
350 }
351
352 pub fn from_file(path: &std::path::Path) -> EdlResult<Self> {
358 let content = std::fs::read_to_string(path)?;
359 let mut edl = parse_edl(&content)?;
360 edl.set_source_file(path.to_path_buf());
361 Ok(edl)
362 }
363
364 pub fn to_file(&self, path: &std::path::Path) -> EdlResult<()> {
370 let generator = EdlGenerator::new();
371 generator.generate_to_file(self, path)
372 }
373}
374
375impl Default for Edl {
376 fn default() -> Self {
377 Self::cmx3600()
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use crate::event::{EditType, TrackType};
385 use crate::timecode::EdlTimecode;
386
387 #[test]
388 fn test_create_edl() {
389 let edl = Edl::new(EdlFormat::Cmx3600);
390 assert_eq!(edl.format, EdlFormat::Cmx3600);
391 assert_eq!(edl.events.len(), 0);
392 }
393
394 #[test]
395 fn test_add_event() {
396 let mut edl = Edl::new(EdlFormat::Cmx3600);
397 edl.set_frame_rate(EdlFrameRate::Fps25);
398
399 let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
400 let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
401
402 let event = EdlEvent::new(
403 1,
404 "A001".to_string(),
405 TrackType::Video,
406 EditType::Cut,
407 tc1,
408 tc2,
409 tc1,
410 tc2,
411 );
412
413 edl.add_event(event).expect("add_event should succeed");
414 assert_eq!(edl.events.len(), 1);
415 }
416
417 #[test]
418 fn test_get_event() {
419 let mut edl = Edl::new(EdlFormat::Cmx3600);
420 edl.set_frame_rate(EdlFrameRate::Fps25);
421
422 let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
423 let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
424
425 let event = EdlEvent::new(
426 1,
427 "A001".to_string(),
428 TrackType::Video,
429 EditType::Cut,
430 tc1,
431 tc2,
432 tc1,
433 tc2,
434 );
435
436 edl.add_event(event).expect("add_event should succeed");
437
438 let retrieved = edl.get_event(1).expect("get_event should succeed");
439 assert_eq!(retrieved.number, 1);
440 }
441
442 #[test]
443 fn test_remove_event() {
444 let mut edl = Edl::new(EdlFormat::Cmx3600);
445 edl.set_frame_rate(EdlFrameRate::Fps25);
446
447 let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
448 let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
449
450 let event = EdlEvent::new(
451 1,
452 "A001".to_string(),
453 TrackType::Video,
454 EditType::Cut,
455 tc1,
456 tc2,
457 tc1,
458 tc2,
459 );
460
461 edl.add_event(event).expect("add_event should succeed");
462 assert_eq!(edl.events.len(), 1);
463
464 edl.remove_event(1);
465 assert_eq!(edl.events.len(), 0);
466 }
467
468 #[test]
469 fn test_renumber_events() {
470 let mut edl = Edl::new(EdlFormat::Cmx3600);
471 edl.set_frame_rate(EdlFrameRate::Fps25);
472
473 let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
474 let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
475
476 let event1 = EdlEvent::new(
477 10,
478 "A001".to_string(),
479 TrackType::Video,
480 EditType::Cut,
481 tc1,
482 tc2,
483 tc1,
484 tc2,
485 );
486
487 let event2 = EdlEvent::new(
488 20,
489 "A002".to_string(),
490 TrackType::Video,
491 EditType::Cut,
492 tc1,
493 tc2,
494 tc1,
495 tc2,
496 );
497
498 edl.add_event(event1).expect("add_event should succeed");
499 edl.add_event(event2).expect("add_event should succeed");
500
501 edl.renumber_events();
502
503 assert_eq!(edl.events[0].number, 1);
504 assert_eq!(edl.events[1].number, 2);
505 }
506
507 #[test]
508 fn test_total_duration() {
509 let mut edl = Edl::new(EdlFormat::Cmx3600);
510 edl.set_frame_rate(EdlFrameRate::Fps25);
511
512 let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
513 let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
514
515 let event = EdlEvent::new(
516 1,
517 "A001".to_string(),
518 TrackType::Video,
519 EditType::Cut,
520 tc1,
521 tc2,
522 tc1,
523 tc2,
524 );
525
526 edl.add_event(event).expect("add_event should succeed");
527
528 let duration_frames = edl.total_duration_frames();
529 assert_eq!(duration_frames, 125); let duration_seconds = edl.total_duration_seconds();
532 assert!((duration_seconds - 5.0).abs() < f64::EPSILON);
533 }
534
535 #[test]
536 fn test_edl_format_display() {
537 assert_eq!(EdlFormat::Cmx3600.to_string(), "CMX 3600");
538 assert_eq!(EdlFormat::Cmx3400.to_string(), "CMX 3400");
539 assert_eq!(EdlFormat::Gvg.to_string(), "GVG");
540 }
541
542 #[test]
543 fn test_parse_and_generate_roundtrip() {
544 let edl_text = r#"TITLE: Test EDL
545FCM: NON-DROP FRAME
546
547001 AX V C 01:00:00:00 01:00:05:00 01:00:00:00 01:00:05:00
548
549"#;
550
551 let edl = parse_edl(edl_text).expect("operation should succeed");
552 let generated = edl.to_string_format().expect("formatting should succeed");
553
554 let edl2 = parse_edl(&generated).expect("operation should succeed");
556
557 assert_eq!(edl.title, edl2.title);
558 assert_eq!(edl.events.len(), edl2.events.len());
559 assert_eq!(edl.events[0].number, edl2.events[0].number);
560 }
561
562 #[test]
566 fn test_cmx3600_roundtrip() {
567 let mut edl = Edl::new(EdlFormat::Cmx3600);
568 edl.set_title("Roundtrip Test EDL".to_string());
569 edl.set_frame_rate(EdlFrameRate::Fps25);
570
571 let reels = ["ALPHA", "BETA", "GAMMA"];
572 for (i, reel) in reels.iter().enumerate() {
573 let n = i as u8;
574 let tc_in = EdlTimecode::new(1, 0, n * 5, 0, EdlFrameRate::Fps25).expect("valid tc_in");
575 let tc_out =
576 EdlTimecode::new(1, 0, n * 5 + 5, 0, EdlFrameRate::Fps25).expect("valid tc_out");
577 let event = EdlEvent::new(
578 (i + 1) as u32,
579 reel.to_string(),
580 TrackType::Video,
581 EditType::Cut,
582 tc_in,
583 tc_out,
584 tc_in,
585 tc_out,
586 );
587 edl.add_event(event).expect("add_event should succeed");
588 }
589
590 assert_eq!(
591 edl.events.len(),
592 3,
593 "EDL should have 3 events before serialisation"
594 );
595
596 let cmx_string = edl
598 .to_string_format()
599 .expect("serialisation should succeed");
600 assert!(
601 cmx_string.contains("TITLE: Roundtrip Test EDL"),
602 "generated text should contain the title"
603 );
604
605 let edl2 = parse_edl(&cmx_string).expect("re-parse should succeed");
607
608 assert_eq!(
610 edl2.events.len(),
611 3,
612 "re-parsed EDL should have 3 events; generated text:\n{cmx_string}"
613 );
614
615 for (orig_event, reparsed_event) in edl.events.iter().zip(edl2.events.iter()) {
617 assert_eq!(
618 orig_event.reel, reparsed_event.reel,
619 "reel name should survive the roundtrip"
620 );
621 }
622 }
623}