use crate::event::EdlEvent;
use crate::Edl;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum EdlChange {
Added {
event: EdlEvent,
},
Removed {
event: EdlEvent,
},
Modified {
old_event: EdlEvent,
new_event: EdlEvent,
field_changes: Vec<FieldChange>,
},
Moved {
old_event: EdlEvent,
new_event: EdlEvent,
},
}
impl EdlChange {
#[must_use]
pub fn summary(&self) -> String {
match self {
Self::Added { event } => {
format!(
"Added event {} (reel: {}, {})",
event.number, event.reel, event.edit_type
)
}
Self::Removed { event } => {
format!(
"Removed event {} (reel: {}, {})",
event.number, event.reel, event.edit_type
)
}
Self::Modified {
old_event,
field_changes,
..
} => {
let changes: Vec<&str> = field_changes.iter().map(|c| c.field_name()).collect();
format!(
"Modified event {}: {}",
old_event.number,
changes.join(", ")
)
}
Self::Moved {
old_event,
new_event,
} => {
format!(
"Moved event {} -> {} (reel: {})",
old_event.number, new_event.number, old_event.reel
)
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FieldChange {
Reel {
old: String,
new: String,
},
EditType {
old: String,
new: String,
},
TrackType {
old: String,
new: String,
},
SourceIn {
old_frames: u64,
new_frames: u64,
},
SourceOut {
old_frames: u64,
new_frames: u64,
},
RecordIn {
old_frames: u64,
new_frames: u64,
},
RecordOut {
old_frames: u64,
new_frames: u64,
},
ClipName {
old: Option<String>,
new: Option<String>,
},
TransitionDuration {
old: Option<u32>,
new: Option<u32>,
},
}
impl FieldChange {
#[must_use]
pub const fn field_name(&self) -> &'static str {
match self {
Self::Reel { .. } => "reel",
Self::EditType { .. } => "edit_type",
Self::TrackType { .. } => "track_type",
Self::SourceIn { .. } => "source_in",
Self::SourceOut { .. } => "source_out",
Self::RecordIn { .. } => "record_in",
Self::RecordOut { .. } => "record_out",
Self::ClipName { .. } => "clip_name",
Self::TransitionDuration { .. } => "transition_duration",
}
}
}
#[derive(Debug, Clone)]
pub struct EdlDiff {
pub old_title: Option<String>,
pub new_title: Option<String>,
pub changes: Vec<EdlChange>,
}
impl EdlDiff {
#[must_use]
pub fn change_count(&self) -> usize {
self.changes.len()
}
#[must_use]
pub fn is_identical(&self) -> bool {
self.changes.is_empty()
}
#[must_use]
pub fn added_count(&self) -> usize {
self.changes
.iter()
.filter(|c| matches!(c, EdlChange::Added { .. }))
.count()
}
#[must_use]
pub fn removed_count(&self) -> usize {
self.changes
.iter()
.filter(|c| matches!(c, EdlChange::Removed { .. }))
.count()
}
#[must_use]
pub fn modified_count(&self) -> usize {
self.changes
.iter()
.filter(|c| matches!(c, EdlChange::Modified { .. }))
.count()
}
#[must_use]
pub fn moved_count(&self) -> usize {
self.changes
.iter()
.filter(|c| matches!(c, EdlChange::Moved { .. }))
.count()
}
#[must_use]
pub fn to_report(&self) -> String {
let mut lines = Vec::new();
lines.push(format!("EDL Diff: {} change(s)", self.changes.len()));
lines.push(format!(
" Added: {}, Removed: {}, Modified: {}, Moved: {}",
self.added_count(),
self.removed_count(),
self.modified_count(),
self.moved_count(),
));
if self.old_title != self.new_title {
lines.push(format!(
" Title: {:?} -> {:?}",
self.old_title, self.new_title
));
}
lines.push(String::new());
for change in &self.changes {
let prefix = match change {
EdlChange::Added { .. } => "+",
EdlChange::Removed { .. } => "-",
EdlChange::Modified { .. } => "~",
EdlChange::Moved { .. } => ">",
};
lines.push(format!("{prefix} {}", change.summary()));
}
lines.join("\n")
}
#[must_use]
pub fn added_events(&self) -> Vec<&EdlEvent> {
self.changes
.iter()
.filter_map(|c| match c {
EdlChange::Added { event } => Some(event),
_ => None,
})
.collect()
}
#[must_use]
pub fn removed_events(&self) -> Vec<&EdlEvent> {
self.changes
.iter()
.filter_map(|c| match c {
EdlChange::Removed { event } => Some(event),
_ => None,
})
.collect()
}
}
#[must_use]
pub fn diff_edls(old: &Edl, new: &Edl) -> EdlDiff {
diff_event_lists(
&old.events,
&new.events,
old.title.clone(),
new.title.clone(),
)
}
#[must_use]
pub fn diff_event_lists(
old: &[EdlEvent],
new: &[EdlEvent],
old_title: Option<String>,
new_title: Option<String>,
) -> EdlDiff {
let mut changes = Vec::new();
let _old_by_num: HashMap<u32, &EdlEvent> = old.iter().map(|e| (e.number, e)).collect();
let new_by_num: HashMap<u32, &EdlEvent> = new.iter().map(|e| (e.number, e)).collect();
let mut matched_new: std::collections::HashSet<u32> = std::collections::HashSet::new();
for old_ev in old {
if let Some(new_ev) = new_by_num.get(&old_ev.number) {
let field_changes = compute_field_changes(old_ev, new_ev);
if !field_changes.is_empty() {
changes.push(EdlChange::Modified {
old_event: old_ev.clone(),
new_event: (*new_ev).clone(),
field_changes,
});
}
matched_new.insert(new_ev.number);
} else {
if let Some(moved_to) = find_moved_event(old_ev, new, &matched_new) {
changes.push(EdlChange::Moved {
old_event: old_ev.clone(),
new_event: moved_to.clone(),
});
matched_new.insert(moved_to.number);
} else {
changes.push(EdlChange::Removed {
event: old_ev.clone(),
});
}
}
}
for new_ev in new {
if !matched_new.contains(&new_ev.number) {
changes.push(EdlChange::Added {
event: new_ev.clone(),
});
}
}
changes.sort_by_key(|c| match c {
EdlChange::Added { event } => event.number,
EdlChange::Removed { event } => event.number,
EdlChange::Modified { old_event, .. } => old_event.number,
EdlChange::Moved { old_event, .. } => old_event.number,
});
EdlDiff {
old_title,
new_title,
changes,
}
}
fn find_moved_event<'a>(
old_ev: &EdlEvent,
new_events: &'a [EdlEvent],
already_matched: &std::collections::HashSet<u32>,
) -> Option<&'a EdlEvent> {
new_events.iter().find(|ne| {
!already_matched.contains(&ne.number)
&& ne.number != old_ev.number
&& ne.reel == old_ev.reel
&& ne.source_in == old_ev.source_in
&& ne.source_out == old_ev.source_out
&& ne.record_in == old_ev.record_in
&& ne.record_out == old_ev.record_out
&& ne.edit_type == old_ev.edit_type
})
}
fn compute_field_changes(old: &EdlEvent, new: &EdlEvent) -> Vec<FieldChange> {
let mut changes = Vec::new();
if old.reel != new.reel {
changes.push(FieldChange::Reel {
old: old.reel.clone(),
new: new.reel.clone(),
});
}
if old.edit_type != new.edit_type {
changes.push(FieldChange::EditType {
old: old.edit_type.to_string(),
new: new.edit_type.to_string(),
});
}
if old.track != new.track {
changes.push(FieldChange::TrackType {
old: old.track.to_string(),
new: new.track.to_string(),
});
}
if old.source_in != new.source_in {
changes.push(FieldChange::SourceIn {
old_frames: old.source_in.to_frames(),
new_frames: new.source_in.to_frames(),
});
}
if old.source_out != new.source_out {
changes.push(FieldChange::SourceOut {
old_frames: old.source_out.to_frames(),
new_frames: new.source_out.to_frames(),
});
}
if old.record_in != new.record_in {
changes.push(FieldChange::RecordIn {
old_frames: old.record_in.to_frames(),
new_frames: new.record_in.to_frames(),
});
}
if old.record_out != new.record_out {
changes.push(FieldChange::RecordOut {
old_frames: old.record_out.to_frames(),
new_frames: new.record_out.to_frames(),
});
}
if old.clip_name != new.clip_name {
changes.push(FieldChange::ClipName {
old: old.clip_name.clone(),
new: new.clip_name.clone(),
});
}
if old.transition_duration != new.transition_duration {
changes.push(FieldChange::TransitionDuration {
old: old.transition_duration,
new: new.transition_duration,
});
}
changes
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{EditType, TrackType};
use crate::timecode::{EdlFrameRate, EdlTimecode};
use crate::EdlFormat;
fn tc(h: u8, m: u8, s: u8, f: u8) -> EdlTimecode {
EdlTimecode::new(h, m, s, f, EdlFrameRate::Fps25).expect("valid timecode")
}
fn make_event(num: u32, reel: &str) -> EdlEvent {
let tc1 = tc(1, 0, 0, 0);
let tc2 = tc(1, 0, 5, 0);
EdlEvent::new(
num,
reel.to_string(),
TrackType::Video,
EditType::Cut,
tc1,
tc2,
tc1,
tc2,
)
}
fn make_event_at(num: u32, reel: &str, s_in: u8, s_out: u8) -> EdlEvent {
EdlEvent::new(
num,
reel.to_string(),
TrackType::Video,
EditType::Cut,
tc(1, 0, s_in, 0),
tc(1, 0, s_out, 0),
tc(1, 0, s_in, 0),
tc(1, 0, s_out, 0),
)
}
fn make_edl(events: Vec<EdlEvent>) -> Edl {
let mut edl = Edl::new(EdlFormat::Cmx3600);
edl.set_frame_rate(EdlFrameRate::Fps25);
edl.events = events;
edl
}
#[test]
fn test_identical_edls() {
let old = make_edl(vec![make_event(1, "A001"), make_event(2, "A002")]);
let new = make_edl(vec![make_event(1, "A001"), make_event(2, "A002")]);
let diff = diff_edls(&old, &new);
assert!(diff.is_identical());
assert_eq!(diff.change_count(), 0);
}
#[test]
fn test_added_event() {
let old = make_edl(vec![make_event(1, "A001")]);
let new = make_edl(vec![make_event(1, "A001"), make_event(2, "A002")]);
let diff = diff_edls(&old, &new);
assert_eq!(diff.added_count(), 1);
assert_eq!(diff.removed_count(), 0);
let added = diff.added_events();
assert_eq!(added[0].number, 2);
assert_eq!(added[0].reel, "A002");
}
#[test]
fn test_removed_event() {
let old = make_edl(vec![make_event(1, "A001"), make_event(2, "A002")]);
let new = make_edl(vec![make_event(1, "A001")]);
let diff = diff_edls(&old, &new);
assert_eq!(diff.removed_count(), 1);
assert_eq!(diff.added_count(), 0);
let removed = diff.removed_events();
assert_eq!(removed[0].number, 2);
}
#[test]
fn test_modified_event_reel_change() {
let old = make_edl(vec![make_event(1, "A001")]);
let new = make_edl(vec![make_event(1, "B001")]);
let diff = diff_edls(&old, &new);
assert_eq!(diff.modified_count(), 1);
if let EdlChange::Modified { field_changes, .. } = &diff.changes[0] {
assert_eq!(field_changes.len(), 1);
assert!(
matches!(&field_changes[0], FieldChange::Reel { old, new } if old == "A001" && new == "B001")
);
} else {
panic!("Expected Modified change");
}
}
#[test]
fn test_moved_event() {
let old = make_edl(vec![
make_event_at(1, "A001", 0, 5),
make_event_at(2, "A002", 5, 10),
]);
let new = make_edl(vec![
make_event_at(3, "A001", 0, 5),
make_event_at(4, "A002", 5, 10),
]);
let diff = diff_edls(&old, &new);
assert_eq!(diff.moved_count(), 2);
}
#[test]
fn test_mixed_changes() {
let old = make_edl(vec![
make_event(1, "A001"),
make_event(2, "A002"),
make_event(3, "A003"),
]);
let new = make_edl(vec![
make_event(1, "A001"), make_event(2, "B002"), make_event(4, "A004"), ]);
let diff = diff_edls(&old, &new);
assert_eq!(diff.modified_count(), 1);
assert_eq!(diff.removed_count(), 1);
assert_eq!(diff.added_count(), 1);
}
#[test]
fn test_diff_report() {
let old = make_edl(vec![make_event(1, "A001")]);
let new = make_edl(vec![make_event(1, "B001")]);
let diff = diff_edls(&old, &new);
let report = diff.to_report();
assert!(report.contains("1 change(s)"));
assert!(report.contains("Modified"));
assert!(report.contains("reel"));
}
#[test]
fn test_empty_edls() {
let old = make_edl(vec![]);
let new = make_edl(vec![]);
let diff = diff_edls(&old, &new);
assert!(diff.is_identical());
}
#[test]
fn test_field_change_names() {
assert_eq!(
FieldChange::Reel {
old: "a".to_string(),
new: "b".to_string()
}
.field_name(),
"reel"
);
assert_eq!(
FieldChange::SourceIn {
old_frames: 0,
new_frames: 1
}
.field_name(),
"source_in"
);
assert_eq!(
FieldChange::RecordOut {
old_frames: 0,
new_frames: 1
}
.field_name(),
"record_out"
);
}
#[test]
fn test_change_summary_text() {
let ev = make_event(1, "A001");
let change = EdlChange::Added { event: ev.clone() };
let summary = change.summary();
assert!(summary.contains("Added"));
assert!(summary.contains("A001"));
let removed = EdlChange::Removed { event: ev };
assert!(removed.summary().contains("Removed"));
}
#[test]
fn test_multiple_field_changes() {
let mut old_ev = make_event(1, "A001");
old_ev.set_clip_name("old_clip.mov".to_string());
let mut new_ev = make_event(1, "B001");
new_ev.set_clip_name("new_clip.mov".to_string());
let changes = compute_field_changes(&old_ev, &new_ev);
assert_eq!(changes.len(), 2); let field_names: Vec<&str> = changes.iter().map(|c| c.field_name()).collect();
assert!(field_names.contains(&"reel"));
assert!(field_names.contains(&"clip_name"));
}
#[test]
fn test_diff_title_change_in_report() {
let mut old = make_edl(vec![]);
old.set_title("Old Title".to_string());
let mut new = make_edl(vec![]);
new.set_title("New Title".to_string());
let diff = diff_edls(&old, &new);
let report = diff.to_report();
assert!(report.contains("Old Title"));
assert!(report.contains("New Title"));
}
#[test]
fn test_diff_event_lists_directly() {
let old = vec![make_event(1, "A001")];
let new = vec![make_event(1, "A001"), make_event(2, "A002")];
let diff = diff_event_lists(&old, &new, None, None);
assert_eq!(diff.added_count(), 1);
}
}