#![allow(dead_code)]
use std::collections::HashSet;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum DiffKind {
Name {
left: String,
right: String,
},
InPoint {
left: u64,
right: u64,
},
OutPoint {
left: u64,
right: u64,
},
Duration {
left: u64,
right: u64,
},
Rating {
left: u8,
right: u8,
},
Keywords {
only_left: Vec<String>,
only_right: Vec<String>,
},
Codec {
left: String,
right: String,
},
Resolution {
left: (u32, u32),
right: (u32, u32),
},
FrameRate {
left: f64,
right: f64,
},
ColorLabel {
left: String,
right: String,
},
Note {
left: String,
right: String,
},
}
impl fmt::Display for DiffKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Name { left, right } => write!(f, "Name: '{left}' vs '{right}'"),
Self::InPoint { left, right } => write!(f, "In Point: {left} vs {right}"),
Self::OutPoint { left, right } => write!(f, "Out Point: {left} vs {right}"),
Self::Duration { left, right } => write!(f, "Duration: {left} vs {right}"),
Self::Rating { left, right } => write!(f, "Rating: {left} vs {right}"),
Self::Keywords {
only_left,
only_right,
} => {
write!(
f,
"Keywords: only left=[{}], only right=[{}]",
only_left.join(", "),
only_right.join(", ")
)
}
Self::Codec { left, right } => write!(f, "Codec: '{left}' vs '{right}'"),
Self::Resolution { left, right } => {
write!(
f,
"Resolution: {}x{} vs {}x{}",
left.0, left.1, right.0, right.1
)
}
Self::FrameRate { left, right } => {
write!(f, "Frame Rate: {left:.3} vs {right:.3}")
}
Self::ColorLabel { left, right } => {
write!(f, "Color Label: '{left}' vs '{right}'")
}
Self::Note { left: _, right: _ } => write!(f, "Note changed"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DiffSeverity {
Info,
Warning,
Critical,
}
#[derive(Debug, Clone)]
pub struct ClipDiff {
pub kind: DiffKind,
pub severity: DiffSeverity,
}
impl ClipDiff {
#[must_use]
pub fn new(kind: DiffKind, severity: DiffSeverity) -> Self {
Self { kind, severity }
}
}
#[derive(Debug, Clone)]
pub struct ComparableClip {
pub id: u64,
pub name: String,
pub in_point: u64,
pub out_point: u64,
pub rating: u8,
pub keywords: Vec<String>,
pub codec: String,
pub width: u32,
pub height: u32,
pub frame_rate: f64,
pub color_label: String,
pub note: String,
}
impl ComparableClip {
#[must_use]
pub fn new(id: u64, name: impl Into<String>) -> Self {
Self {
id,
name: name.into(),
in_point: 0,
out_point: 0,
rating: 0,
keywords: Vec::new(),
codec: String::new(),
width: 0,
height: 0,
frame_rate: 0.0,
color_label: String::new(),
note: String::new(),
}
}
#[must_use]
pub fn duration(&self) -> u64 {
self.out_point.saturating_sub(self.in_point)
}
}
#[derive(Debug, Clone)]
pub struct CompareResult {
pub left_id: u64,
pub right_id: u64,
pub diffs: Vec<ClipDiff>,
}
impl CompareResult {
#[must_use]
pub fn new(left_id: u64, right_id: u64) -> Self {
Self {
left_id,
right_id,
diffs: Vec::new(),
}
}
#[must_use]
pub fn is_identical(&self) -> bool {
self.diffs.is_empty()
}
#[must_use]
pub fn diff_count(&self) -> usize {
self.diffs.len()
}
#[must_use]
pub fn critical_diffs(&self) -> Vec<&ClipDiff> {
self.diffs
.iter()
.filter(|d| d.severity == DiffSeverity::Critical)
.collect()
}
#[must_use]
pub fn max_severity(&self) -> Option<DiffSeverity> {
self.diffs.iter().map(|d| d.severity).max()
}
#[must_use]
pub fn summary(&self) -> String {
if self.is_identical() {
return format!("Clips {} and {} are identical", self.left_id, self.right_id);
}
let mut lines = vec![format!(
"Comparing clip {} vs {}: {} difference(s)",
self.left_id,
self.right_id,
self.diff_count()
)];
for diff in &self.diffs {
lines.push(format!(" [{:?}] {}", diff.severity, diff.kind));
}
lines.join("\n")
}
}
#[derive(Debug)]
pub struct ClipComparer {
pub compare_names: bool,
pub compare_trim: bool,
pub compare_ratings: bool,
pub compare_keywords: bool,
pub compare_technical: bool,
pub compare_notes: bool,
pub fps_tolerance: f64,
}
impl Default for ClipComparer {
fn default() -> Self {
Self {
compare_names: true,
compare_trim: true,
compare_ratings: true,
compare_keywords: true,
compare_technical: true,
compare_notes: true,
fps_tolerance: 0.001,
}
}
}
impl ClipComparer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn technical_only() -> Self {
Self {
compare_names: false,
compare_trim: true,
compare_ratings: false,
compare_keywords: false,
compare_technical: true,
compare_notes: false,
fps_tolerance: 0.001,
}
}
#[must_use]
pub fn compare(&self, left: &ComparableClip, right: &ComparableClip) -> CompareResult {
let mut result = CompareResult::new(left.id, right.id);
if self.compare_names && left.name != right.name {
result.diffs.push(ClipDiff::new(
DiffKind::Name {
left: left.name.clone(),
right: right.name.clone(),
},
DiffSeverity::Info,
));
}
if self.compare_trim {
if left.in_point != right.in_point {
result.diffs.push(ClipDiff::new(
DiffKind::InPoint {
left: left.in_point,
right: right.in_point,
},
DiffSeverity::Critical,
));
}
if left.out_point != right.out_point {
result.diffs.push(ClipDiff::new(
DiffKind::OutPoint {
left: left.out_point,
right: right.out_point,
},
DiffSeverity::Critical,
));
}
let ld = left.duration();
let rd = right.duration();
if ld != rd {
result.diffs.push(ClipDiff::new(
DiffKind::Duration {
left: ld,
right: rd,
},
DiffSeverity::Critical,
));
}
}
if self.compare_ratings && left.rating != right.rating {
result.diffs.push(ClipDiff::new(
DiffKind::Rating {
left: left.rating,
right: right.rating,
},
DiffSeverity::Warning,
));
}
if self.compare_keywords {
let left_set: HashSet<&str> = left.keywords.iter().map(|s| s.as_str()).collect();
let right_set: HashSet<&str> = right.keywords.iter().map(|s| s.as_str()).collect();
let only_left: Vec<String> = left_set
.difference(&right_set)
.map(|s| (*s).to_string())
.collect();
let only_right: Vec<String> = right_set
.difference(&left_set)
.map(|s| (*s).to_string())
.collect();
if !only_left.is_empty() || !only_right.is_empty() {
result.diffs.push(ClipDiff::new(
DiffKind::Keywords {
only_left,
only_right,
},
DiffSeverity::Warning,
));
}
}
if self.compare_technical {
if left.codec != right.codec {
result.diffs.push(ClipDiff::new(
DiffKind::Codec {
left: left.codec.clone(),
right: right.codec.clone(),
},
DiffSeverity::Critical,
));
}
if left.width != right.width || left.height != right.height {
result.diffs.push(ClipDiff::new(
DiffKind::Resolution {
left: (left.width, left.height),
right: (right.width, right.height),
},
DiffSeverity::Critical,
));
}
if (left.frame_rate - right.frame_rate).abs() > self.fps_tolerance {
result.diffs.push(ClipDiff::new(
DiffKind::FrameRate {
left: left.frame_rate,
right: right.frame_rate,
},
DiffSeverity::Critical,
));
}
}
if self.compare_notes && left.note != right.note {
result.diffs.push(ClipDiff::new(
DiffKind::Note {
left: left.note.clone(),
right: right.note.clone(),
},
DiffSeverity::Info,
));
}
if self.compare_names && left.color_label != right.color_label {
result.diffs.push(ClipDiff::new(
DiffKind::ColorLabel {
left: left.color_label.clone(),
right: right.color_label.clone(),
},
DiffSeverity::Info,
));
}
result
}
#[must_use]
pub fn compare_against_reference(
&self,
reference: &ComparableClip,
others: &[ComparableClip],
) -> Vec<CompareResult> {
others
.iter()
.map(|other| self.compare(reference, other))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_clip_a() -> ComparableClip {
let mut c = ComparableClip::new(1, "Clip A");
c.in_point = 0;
c.out_point = 100;
c.rating = 4;
c.keywords = vec!["interview".into(), "john".into()];
c.codec = "H.264".into();
c.width = 1920;
c.height = 1080;
c.frame_rate = 24.0;
c.color_label = "blue".into();
c.note = "Good take".into();
c
}
fn make_clip_b() -> ComparableClip {
let mut c = ComparableClip::new(2, "Clip B");
c.in_point = 10;
c.out_point = 110;
c.rating = 3;
c.keywords = vec!["interview".into(), "jane".into()];
c.codec = "H.264".into();
c.width = 1920;
c.height = 1080;
c.frame_rate = 24.0;
c.color_label = "red".into();
c.note = "Needs retake".into();
c
}
#[test]
fn test_comparable_clip_duration() {
let c = make_clip_a();
assert_eq!(c.duration(), 100);
}
#[test]
fn test_identical_clips() {
let a = make_clip_a();
let b = make_clip_a();
let comparer = ClipComparer::new();
let result = comparer.compare(&a, &b);
assert!(result.is_identical());
}
#[test]
fn test_name_diff() {
let a = make_clip_a();
let b = make_clip_b();
let comparer = ClipComparer::new();
let result = comparer.compare(&a, &b);
assert!(!result.is_identical());
let has_name = result
.diffs
.iter()
.any(|d| matches!(&d.kind, DiffKind::Name { .. }));
assert!(has_name);
}
#[test]
fn test_trim_diff() {
let a = make_clip_a();
let b = make_clip_b();
let comparer = ClipComparer::new();
let result = comparer.compare(&a, &b);
let has_in = result
.diffs
.iter()
.any(|d| matches!(&d.kind, DiffKind::InPoint { .. }));
assert!(has_in);
}
#[test]
fn test_rating_diff() {
let a = make_clip_a();
let b = make_clip_b();
let comparer = ClipComparer::new();
let result = comparer.compare(&a, &b);
let has_rating = result
.diffs
.iter()
.any(|d| matches!(&d.kind, DiffKind::Rating { .. }));
assert!(has_rating);
}
#[test]
fn test_keyword_diff() {
let a = make_clip_a();
let b = make_clip_b();
let comparer = ClipComparer::new();
let result = comparer.compare(&a, &b);
let kw_diff = result
.diffs
.iter()
.find(|d| matches!(&d.kind, DiffKind::Keywords { .. }));
assert!(kw_diff.is_some());
}
#[test]
fn test_technical_only_comparer() {
let a = make_clip_a();
let b = make_clip_b();
let comparer = ClipComparer::technical_only();
let result = comparer.compare(&a, &b);
let has_name = result
.diffs
.iter()
.any(|d| matches!(&d.kind, DiffKind::Name { .. }));
assert!(!has_name);
let has_rating = result
.diffs
.iter()
.any(|d| matches!(&d.kind, DiffKind::Rating { .. }));
assert!(!has_rating);
}
#[test]
fn test_diff_severity_ordering() {
assert!(DiffSeverity::Info < DiffSeverity::Warning);
assert!(DiffSeverity::Warning < DiffSeverity::Critical);
}
#[test]
fn test_max_severity() {
let a = make_clip_a();
let b = make_clip_b();
let comparer = ClipComparer::new();
let result = comparer.compare(&a, &b);
let max = result.max_severity().expect("max_severity should succeed");
assert_eq!(max, DiffSeverity::Critical);
}
#[test]
fn test_critical_diffs() {
let a = make_clip_a();
let b = make_clip_b();
let comparer = ClipComparer::new();
let result = comparer.compare(&a, &b);
let critical = result.critical_diffs();
assert!(!critical.is_empty());
for d in &critical {
assert_eq!(d.severity, DiffSeverity::Critical);
}
}
#[test]
fn test_summary_identical() {
let a = make_clip_a();
let b = make_clip_a();
let comparer = ClipComparer::new();
let result = comparer.compare(&a, &b);
let summary = result.summary();
assert!(summary.contains("identical"));
}
#[test]
fn test_summary_with_diffs() {
let a = make_clip_a();
let b = make_clip_b();
let comparer = ClipComparer::new();
let result = comparer.compare(&a, &b);
let summary = result.summary();
assert!(summary.contains("difference(s)"));
}
#[test]
fn test_compare_against_reference() {
let reference = make_clip_a();
let others = vec![make_clip_b(), make_clip_a()];
let comparer = ClipComparer::new();
let results = comparer.compare_against_reference(&reference, &others);
assert_eq!(results.len(), 2);
assert!(!results[0].is_identical()); assert!(results[1].is_identical()); }
#[test]
fn test_diff_kind_display() {
let dk = DiffKind::InPoint { left: 0, right: 10 };
let s = format!("{dk}");
assert!(s.contains("0"));
assert!(s.contains("10"));
}
#[test]
fn test_frame_rate_tolerance() {
let mut a = make_clip_a();
let mut b = make_clip_a();
a.frame_rate = 23.976;
b.frame_rate = 23.976_024; let comparer = ClipComparer {
fps_tolerance: 0.001,
..ClipComparer::new()
};
let result = comparer.compare(&a, &b);
let has_fps = result
.diffs
.iter()
.any(|d| matches!(&d.kind, DiffKind::FrameRate { .. }));
assert!(!has_fps);
}
}