#![allow(dead_code)]
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum ClipAttribute {
Codec(String),
FrameRate(u32, u32),
Resolution(u32, u32),
BitDepth(u8),
Description(String),
Tag(String),
Rating(u8),
ReviewFlag(bool),
}
impl ClipAttribute {
#[must_use]
pub fn is_technical(&self) -> bool {
matches!(
self,
Self::Codec(_) | Self::FrameRate(_, _) | Self::Resolution(_, _) | Self::BitDepth(_)
)
}
#[must_use]
pub fn kind_key(&self) -> &'static str {
match self {
Self::Codec(_) => "codec",
Self::FrameRate(_, _) => "frame_rate",
Self::Resolution(_, _) => "resolution",
Self::BitDepth(_) => "bit_depth",
Self::Description(_) => "description",
Self::Tag(_) => "tag",
Self::Rating(_) => "rating",
Self::ReviewFlag(_) => "review_flag",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ClipMetadata {
pub clip_id: u64,
attributes: HashMap<String, ClipAttribute>,
}
impl ClipMetadata {
#[must_use]
pub fn new(clip_id: u64) -> Self {
Self {
clip_id,
attributes: HashMap::new(),
}
}
pub fn set_attribute(&mut self, attr: ClipAttribute) {
let key = attr.kind_key().to_string();
self.attributes.insert(key, attr);
}
#[must_use]
pub fn has_attribute(&self, kind: &str) -> bool {
self.attributes.contains_key(kind)
}
#[must_use]
pub fn get_attribute(&self, kind: &str) -> Option<&ClipAttribute> {
self.attributes.get(kind)
}
#[must_use]
pub fn attribute_count(&self) -> usize {
self.attributes.len()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &ClipAttribute)> {
self.attributes.iter().map(|(k, v)| (k.as_str(), v))
}
}
#[derive(Debug, Clone, Default)]
pub struct ClipMetadataSet {
entries: HashMap<u64, ClipMetadata>,
}
impl ClipMetadataSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, meta: ClipMetadata) {
self.entries.insert(meta.clip_id, meta);
}
#[must_use]
pub fn get(&self, clip_id: u64) -> Option<&ClipMetadata> {
self.entries.get(&clip_id)
}
pub fn merge(&mut self, other: &Self) {
for (clip_id, other_meta) in &other.entries {
let entry = self
.entries
.entry(*clip_id)
.or_insert_with(|| ClipMetadata::new(*clip_id));
for (_, attr) in other_meta.iter() {
entry.set_attribute(attr.clone());
}
}
}
#[must_use]
pub fn diff(&self, other: &Self) -> Vec<(u64, String)> {
let mut diffs = Vec::new();
for (clip_id, meta) in &self.entries {
let other_meta = other.entries.get(clip_id);
for (key, attr) in meta.iter() {
let matches = other_meta
.and_then(|m| m.get_attribute(key))
.map_or(false, |a| a == attr);
if !matches {
diffs.push((*clip_id, key.to_string()));
}
}
}
for (clip_id, meta) in &other.entries {
let self_meta = self.entries.get(clip_id);
for (key, _) in meta.iter() {
let in_self = self_meta.map_or(false, |m| m.has_attribute(key));
if !in_self {
let entry = (*clip_id, key.to_string());
if !diffs.contains(&entry) {
diffs.push(entry);
}
}
}
}
diffs
}
#[must_use]
pub fn clip_count(&self) -> usize {
self.entries.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_codec_is_technical() {
assert!(ClipAttribute::Codec("h264".into()).is_technical());
}
#[test]
fn test_frame_rate_is_technical() {
assert!(ClipAttribute::FrameRate(25, 1).is_technical());
}
#[test]
fn test_description_not_technical() {
assert!(!ClipAttribute::Description("nice shot".into()).is_technical());
}
#[test]
fn test_tag_not_technical() {
assert!(!ClipAttribute::Tag("hero".into()).is_technical());
}
#[test]
fn test_rating_not_technical() {
assert!(!ClipAttribute::Rating(80).is_technical());
}
#[test]
fn test_clip_metadata_has_attribute_after_set() {
let mut m = ClipMetadata::new(1);
m.set_attribute(ClipAttribute::Codec("prores".into()));
assert!(m.has_attribute("codec"));
}
#[test]
fn test_clip_metadata_missing_attribute() {
let m = ClipMetadata::new(1);
assert!(!m.has_attribute("codec"));
}
#[test]
fn test_clip_metadata_attribute_count() {
let mut m = ClipMetadata::new(1);
m.set_attribute(ClipAttribute::Codec("h264".into()));
m.set_attribute(ClipAttribute::BitDepth(8));
assert_eq!(m.attribute_count(), 2);
}
#[test]
fn test_clip_metadata_overwrite() {
let mut m = ClipMetadata::new(1);
m.set_attribute(ClipAttribute::Codec("h264".into()));
m.set_attribute(ClipAttribute::Codec("hevc".into()));
assert_eq!(m.attribute_count(), 1);
assert_eq!(
m.get_attribute("codec"),
Some(&ClipAttribute::Codec("hevc".into()))
);
}
#[test]
fn test_set_merge_overwrites() {
let mut base = ClipMetadataSet::new();
let mut m1 = ClipMetadata::new(10);
m1.set_attribute(ClipAttribute::Codec("h264".into()));
base.insert(m1);
let mut overlay = ClipMetadataSet::new();
let mut m2 = ClipMetadata::new(10);
m2.set_attribute(ClipAttribute::Codec("hevc".into()));
overlay.insert(m2);
base.merge(&overlay);
let merged = base.get(10).expect("get should succeed");
assert_eq!(
merged.get_attribute("codec"),
Some(&ClipAttribute::Codec("hevc".into()))
);
}
#[test]
fn test_set_merge_adds_new_clip() {
let mut base = ClipMetadataSet::new();
let mut overlay = ClipMetadataSet::new();
let mut m = ClipMetadata::new(99);
m.set_attribute(ClipAttribute::BitDepth(10));
overlay.insert(m);
base.merge(&overlay);
assert!(base.get(99).is_some());
}
#[test]
fn test_diff_same_sets_empty() {
let mut s1 = ClipMetadataSet::new();
let mut m = ClipMetadata::new(1);
m.set_attribute(ClipAttribute::Rating(90));
s1.insert(m);
let s2 = s1.clone();
assert!(s1.diff(&s2).is_empty());
}
#[test]
fn test_diff_detects_difference() {
let mut s1 = ClipMetadataSet::new();
let mut m1 = ClipMetadata::new(1);
m1.set_attribute(ClipAttribute::Rating(90));
s1.insert(m1);
let mut s2 = ClipMetadataSet::new();
let mut m2 = ClipMetadata::new(1);
m2.set_attribute(ClipAttribute::Rating(50));
s2.insert(m2);
let diffs = s1.diff(&s2);
assert!(!diffs.is_empty());
}
#[test]
fn test_clip_count() {
let mut s = ClipMetadataSet::new();
s.insert(ClipMetadata::new(1));
s.insert(ClipMetadata::new(2));
assert_eq!(s.clip_count(), 2);
}
}