use crate::dictionary::Auid;
use crate::timeline::{EditRate, Position};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Comment {
pub category: Option<String>,
pub name: String,
pub value: String,
}
impl Comment {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
category: None,
name: name.into(),
value: value.into(),
}
}
pub fn with_category(mut self, category: impl Into<String>) -> Self {
self.category = Some(category.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaggedValue {
pub name: String,
pub value: TaggedValueData,
}
impl TaggedValue {
pub fn new(name: impl Into<String>, value: TaggedValueData) -> Self {
Self {
name: name.into(),
value,
}
}
pub fn string(name: impl Into<String>, value: impl Into<String>) -> Self {
Self::new(name, TaggedValueData::String(value.into()))
}
pub fn integer(name: impl Into<String>, value: i64) -> Self {
Self::new(name, TaggedValueData::Integer(value))
}
pub fn float(name: impl Into<String>, value: f64) -> Self {
Self::new(name, TaggedValueData::Float(value))
}
pub fn boolean(name: impl Into<String>, value: bool) -> Self {
Self::new(name, TaggedValueData::Boolean(value))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TaggedValueData {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Binary(Vec<u8>),
Auid(Auid),
Rational(i64, i64),
}
#[derive(Debug, Clone)]
pub struct KlvData {
pub key: Vec<u8>,
pub value: Vec<u8>,
}
impl KlvData {
#[must_use]
pub fn new(key: Vec<u8>, value: Vec<u8>) -> Self {
Self { key, value }
}
#[must_use]
pub fn key(&self) -> &[u8] {
&self.key
}
#[must_use]
pub fn value(&self) -> &[u8] {
&self.value
}
#[must_use]
pub fn key_length(&self) -> usize {
self.key.len()
}
#[must_use]
pub fn value_length(&self) -> usize {
self.value.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Timecode {
pub hours: u8,
pub minutes: u8,
pub seconds: u8,
pub frames: u8,
pub drop_frame: bool,
pub fps: u8,
}
impl Timecode {
#[must_use]
pub fn new(hours: u8, minutes: u8, seconds: u8, frames: u8, fps: u8, drop_frame: bool) -> Self {
Self {
hours,
minutes,
seconds,
frames,
drop_frame,
fps,
}
}
#[must_use]
pub fn from_position(position: Position, edit_rate: EditRate) -> Self {
let fps = edit_rate.to_float().round() as u8;
let total_frames = position.to_frames(edit_rate);
let hours = (total_frames / (i64::from(fps) * 3600)) as u8;
let remaining = total_frames % (i64::from(fps) * 3600);
let minutes = (remaining / (i64::from(fps) * 60)) as u8;
let remaining = remaining % (i64::from(fps) * 60);
let seconds = (remaining / i64::from(fps)) as u8;
let frames = (remaining % i64::from(fps)) as u8;
Self {
hours,
minutes,
seconds,
frames,
drop_frame: edit_rate.is_ntsc(),
fps,
}
}
#[must_use]
pub fn to_position(&self, edit_rate: EditRate) -> Position {
let fps = i64::from(self.fps);
let total_frames = i64::from(self.hours) * 3600 * fps
+ i64::from(self.minutes) * 60 * fps
+ i64::from(self.seconds) * fps
+ i64::from(self.frames);
Position::from_frames(total_frames, edit_rate)
}
pub fn parse(s: &str, fps: u8) -> Result<Self, MetadataError> {
let parts: Vec<&str> = s.split(&[':', ';'][..]).collect();
if parts.len() != 4 {
return Err(MetadataError::InvalidTimecode(s.to_string()));
}
let hours = parts[0]
.parse::<u8>()
.map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
let minutes = parts[1]
.parse::<u8>()
.map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
let seconds = parts[2]
.parse::<u8>()
.map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
let frames = parts[3]
.parse::<u8>()
.map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
let drop_frame = s.contains(';');
Ok(Self {
hours,
minutes,
seconds,
frames,
drop_frame,
fps,
})
}
}
impl std::fmt::Display for Timecode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let separator = if self.drop_frame { ';' } else { ':' };
write!(
f,
"{:02}:{:02}:{:02}{}{:02}",
self.hours, self.minutes, self.seconds, separator, self.frames
)
}
}
#[derive(Debug, Clone)]
pub struct DescriptiveMetadata {
items: HashMap<String, MetadataValue>,
linked_objects: Vec<DescriptiveObjectReference>,
}
impl DescriptiveMetadata {
#[must_use]
pub fn new() -> Self {
Self {
items: HashMap::new(),
linked_objects: Vec::new(),
}
}
pub fn add_item(&mut self, key: impl Into<String>, value: MetadataValue) {
self.items.insert(key.into(), value);
}
#[must_use]
pub fn get_item(&self, key: &str) -> Option<&MetadataValue> {
self.items.get(key)
}
pub fn add_linked_object(&mut self, reference: DescriptiveObjectReference) {
self.linked_objects.push(reference);
}
#[must_use]
pub fn items(&self) -> &HashMap<String, MetadataValue> {
&self.items
}
#[must_use]
pub fn linked_objects(&self) -> &[DescriptiveObjectReference] {
&self.linked_objects
}
}
impl Default for DescriptiveMetadata {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MetadataValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
DateTime(String),
Uri(String),
Array(Vec<MetadataValue>),
Object(HashMap<String, MetadataValue>),
}
#[derive(Debug, Clone)]
pub struct DescriptiveObjectReference {
pub object_id: String,
pub object_type: String,
}
#[derive(Debug, Clone)]
pub struct ProductionMetadata {
pub title: Option<String>,
pub episode_title: Option<String>,
pub series_title: Option<String>,
pub production_number: Option<String>,
pub copyright: Option<String>,
pub creation_date: Option<String>,
pub production_company: Option<String>,
pub director: Option<String>,
pub producer: Option<String>,
pub additional: HashMap<String, String>,
}
impl ProductionMetadata {
#[must_use]
pub fn new() -> Self {
Self {
title: None,
episode_title: None,
series_title: None,
production_number: None,
copyright: None,
creation_date: None,
production_company: None,
director: None,
producer: None,
additional: HashMap::new(),
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_company(mut self, company: impl Into<String>) -> Self {
self.production_company = Some(company.into());
self
}
pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.additional.insert(key.into(), value.into());
}
}
impl Default for ProductionMetadata {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct TechnicalMetadata {
pub video_format: Option<String>,
pub audio_format: Option<String>,
pub frame_rate: Option<EditRate>,
pub aspect_ratio: Option<String>,
pub resolution: Option<(u32, u32)>,
pub duration: Option<i64>,
pub file_size: Option<u64>,
pub codec: Option<String>,
pub additional: HashMap<String, String>,
}
impl TechnicalMetadata {
#[must_use]
pub fn new() -> Self {
Self {
video_format: None,
audio_format: None,
frame_rate: None,
aspect_ratio: None,
resolution: None,
duration: None,
file_size: None,
codec: None,
additional: HashMap::new(),
}
}
pub fn with_video_format(mut self, format: impl Into<String>) -> Self {
self.video_format = Some(format.into());
self
}
#[must_use]
pub fn with_frame_rate(mut self, rate: EditRate) -> Self {
self.frame_rate = Some(rate);
self
}
#[must_use]
pub fn with_resolution(mut self, width: u32, height: u32) -> Self {
self.resolution = Some((width, height));
self
}
}
impl Default for TechnicalMetadata {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetadataError {
InvalidTimecode(String),
InvalidValue(String),
NotFound(String),
}
impl std::fmt::Display for MetadataError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MetadataError::InvalidTimecode(s) => write!(f, "Invalid timecode: {s}"),
MetadataError::InvalidValue(s) => write!(f, "Invalid metadata value: {s}"),
MetadataError::NotFound(s) => write!(f, "Metadata not found: {s}"),
}
}
}
impl std::error::Error for MetadataError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_comment() {
let comment = Comment::new("Author", "John Doe").with_category("Production");
assert_eq!(comment.name, "Author");
assert_eq!(comment.value, "John Doe");
assert_eq!(comment.category, Some("Production".to_string()));
}
#[test]
fn test_tagged_value() {
let tv_str = TaggedValue::string("Title", "My Video");
assert_eq!(tv_str.name, "Title");
if let TaggedValueData::String(s) = &tv_str.value {
assert_eq!(s, "My Video");
} else {
panic!("Expected string value");
}
let tv_int = TaggedValue::integer("FrameCount", 1000);
assert_eq!(tv_int.name, "FrameCount");
}
#[test]
fn test_klv_data() {
let key = vec![1, 2, 3, 4];
let value = vec![5, 6, 7, 8, 9];
let klv = KlvData::new(key.clone(), value.clone());
assert_eq!(klv.key(), &key);
assert_eq!(klv.value(), &value);
assert_eq!(klv.key_length(), 4);
assert_eq!(klv.value_length(), 5);
}
#[test]
fn test_timecode() {
let tc = Timecode::new(1, 2, 3, 4, 25, false);
assert_eq!(tc.hours, 1);
assert_eq!(tc.minutes, 2);
assert_eq!(tc.seconds, 3);
assert_eq!(tc.frames, 4);
assert_eq!(tc.to_string(), "01:02:03:04");
}
#[test]
fn test_timecode_parse() {
let tc = Timecode::parse("01:02:03:04", 25).unwrap();
assert_eq!(tc.hours, 1);
assert_eq!(tc.minutes, 2);
assert_eq!(tc.seconds, 3);
assert_eq!(tc.frames, 4);
assert!(!tc.drop_frame);
let tc_df = Timecode::parse("01:02:03;04", 30).unwrap();
assert!(tc_df.drop_frame);
}
#[test]
fn test_timecode_position_conversion() {
let edit_rate = EditRate::new(25, 1);
let tc = Timecode::new(0, 0, 1, 0, 25, false);
let pos = tc.to_position(edit_rate);
assert_eq!(pos.0, 25);
let tc2 = Timecode::from_position(pos, edit_rate);
assert_eq!(tc2.seconds, 1);
assert_eq!(tc2.frames, 0);
}
#[test]
fn test_descriptive_metadata() {
let mut dm = DescriptiveMetadata::new();
dm.add_item("title", MetadataValue::String("My Film".to_string()));
dm.add_item("year", MetadataValue::Integer(2024));
assert!(dm.get_item("title").is_some());
assert!(dm.get_item("year").is_some());
assert_eq!(dm.items().len(), 2);
}
#[test]
fn test_production_metadata() {
let pm = ProductionMetadata::new()
.with_title("Episode 1")
.with_company("ABC Productions");
assert_eq!(pm.title, Some("Episode 1".to_string()));
assert_eq!(pm.production_company, Some("ABC Productions".to_string()));
}
#[test]
fn test_technical_metadata() {
let tm = TechnicalMetadata::new()
.with_video_format("HD")
.with_frame_rate(EditRate::new(25, 1))
.with_resolution(1920, 1080);
assert_eq!(tm.video_format, Some("HD".to_string()));
assert_eq!(tm.resolution, Some((1920, 1080)));
}
}