1use crate::dictionary::Auid;
11use crate::timeline::{EditRate, Position};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Comment {
18 pub category: Option<String>,
20 pub name: String,
22 pub value: String,
24}
25
26impl Comment {
27 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
29 Self {
30 category: None,
31 name: name.into(),
32 value: value.into(),
33 }
34 }
35
36 pub fn with_category(mut self, category: impl Into<String>) -> Self {
38 self.category = Some(category.into());
39 self
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct TaggedValue {
46 pub name: String,
48 pub value: TaggedValueData,
50}
51
52impl TaggedValue {
53 pub fn new(name: impl Into<String>, value: TaggedValueData) -> Self {
55 Self {
56 name: name.into(),
57 value,
58 }
59 }
60
61 pub fn string(name: impl Into<String>, value: impl Into<String>) -> Self {
63 Self::new(name, TaggedValueData::String(value.into()))
64 }
65
66 pub fn integer(name: impl Into<String>, value: i64) -> Self {
68 Self::new(name, TaggedValueData::Integer(value))
69 }
70
71 pub fn float(name: impl Into<String>, value: f64) -> Self {
73 Self::new(name, TaggedValueData::Float(value))
74 }
75
76 pub fn boolean(name: impl Into<String>, value: bool) -> Self {
78 Self::new(name, TaggedValueData::Boolean(value))
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub enum TaggedValueData {
85 String(String),
87 Integer(i64),
89 Float(f64),
91 Boolean(bool),
93 Binary(Vec<u8>),
95 Auid(Auid),
97 Rational(i64, i64),
99}
100
101#[derive(Debug, Clone)]
103pub struct KlvData {
104 pub key: Vec<u8>,
106 pub value: Vec<u8>,
108}
109
110impl KlvData {
111 #[must_use]
113 pub fn new(key: Vec<u8>, value: Vec<u8>) -> Self {
114 Self { key, value }
115 }
116
117 #[must_use]
119 pub fn key(&self) -> &[u8] {
120 &self.key
121 }
122
123 #[must_use]
125 pub fn value(&self) -> &[u8] {
126 &self.value
127 }
128
129 #[must_use]
131 pub fn key_length(&self) -> usize {
132 self.key.len()
133 }
134
135 #[must_use]
137 pub fn value_length(&self) -> usize {
138 self.value.len()
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
144pub struct Timecode {
145 pub hours: u8,
147 pub minutes: u8,
149 pub seconds: u8,
151 pub frames: u8,
153 pub drop_frame: bool,
155 pub fps: u8,
157}
158
159impl Timecode {
160 #[must_use]
162 pub fn new(hours: u8, minutes: u8, seconds: u8, frames: u8, fps: u8, drop_frame: bool) -> Self {
163 Self {
164 hours,
165 minutes,
166 seconds,
167 frames,
168 drop_frame,
169 fps,
170 }
171 }
172
173 #[must_use]
175 pub fn from_position(position: Position, edit_rate: EditRate) -> Self {
176 let fps = edit_rate.to_float().round() as u8;
177 let total_frames = position.to_frames(edit_rate);
178
179 let hours = (total_frames / (i64::from(fps) * 3600)) as u8;
180 let remaining = total_frames % (i64::from(fps) * 3600);
181 let minutes = (remaining / (i64::from(fps) * 60)) as u8;
182 let remaining = remaining % (i64::from(fps) * 60);
183 let seconds = (remaining / i64::from(fps)) as u8;
184 let frames = (remaining % i64::from(fps)) as u8;
185
186 Self {
187 hours,
188 minutes,
189 seconds,
190 frames,
191 drop_frame: edit_rate.is_ntsc(),
192 fps,
193 }
194 }
195
196 #[must_use]
198 pub fn to_position(&self, edit_rate: EditRate) -> Position {
199 let fps = i64::from(self.fps);
200 let total_frames = i64::from(self.hours) * 3600 * fps
201 + i64::from(self.minutes) * 60 * fps
202 + i64::from(self.seconds) * fps
203 + i64::from(self.frames);
204
205 Position::from_frames(total_frames, edit_rate)
206 }
207
208 pub fn parse(s: &str, fps: u8) -> Result<Self, MetadataError> {
210 let parts: Vec<&str> = s.split(&[':', ';'][..]).collect();
211 if parts.len() != 4 {
212 return Err(MetadataError::InvalidTimecode(s.to_string()));
213 }
214
215 let hours = parts[0]
216 .parse::<u8>()
217 .map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
218 let minutes = parts[1]
219 .parse::<u8>()
220 .map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
221 let seconds = parts[2]
222 .parse::<u8>()
223 .map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
224 let frames = parts[3]
225 .parse::<u8>()
226 .map_err(|_| MetadataError::InvalidTimecode(s.to_string()))?;
227
228 let drop_frame = s.contains(';');
229
230 Ok(Self {
231 hours,
232 minutes,
233 seconds,
234 frames,
235 drop_frame,
236 fps,
237 })
238 }
239}
240
241impl std::fmt::Display for Timecode {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 let separator = if self.drop_frame { ';' } else { ':' };
244 write!(
245 f,
246 "{:02}:{:02}:{:02}{}{:02}",
247 self.hours, self.minutes, self.seconds, separator, self.frames
248 )
249 }
250}
251
252#[derive(Debug, Clone)]
254pub struct DescriptiveMetadata {
255 items: HashMap<String, MetadataValue>,
257 linked_objects: Vec<DescriptiveObjectReference>,
259}
260
261impl DescriptiveMetadata {
262 #[must_use]
264 pub fn new() -> Self {
265 Self {
266 items: HashMap::new(),
267 linked_objects: Vec::new(),
268 }
269 }
270
271 pub fn add_item(&mut self, key: impl Into<String>, value: MetadataValue) {
273 self.items.insert(key.into(), value);
274 }
275
276 #[must_use]
278 pub fn get_item(&self, key: &str) -> Option<&MetadataValue> {
279 self.items.get(key)
280 }
281
282 pub fn add_linked_object(&mut self, reference: DescriptiveObjectReference) {
284 self.linked_objects.push(reference);
285 }
286
287 #[must_use]
289 pub fn items(&self) -> &HashMap<String, MetadataValue> {
290 &self.items
291 }
292
293 #[must_use]
295 pub fn linked_objects(&self) -> &[DescriptiveObjectReference] {
296 &self.linked_objects
297 }
298}
299
300impl Default for DescriptiveMetadata {
301 fn default() -> Self {
302 Self::new()
303 }
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308pub enum MetadataValue {
309 String(String),
311 Integer(i64),
313 Float(f64),
315 Boolean(bool),
317 DateTime(String),
319 Uri(String),
321 Array(Vec<MetadataValue>),
323 Object(HashMap<String, MetadataValue>),
325}
326
327#[derive(Debug, Clone)]
329pub struct DescriptiveObjectReference {
330 pub object_id: String,
332 pub object_type: String,
334}
335
336#[derive(Debug, Clone)]
338pub struct ProductionMetadata {
339 pub title: Option<String>,
341 pub episode_title: Option<String>,
343 pub series_title: Option<String>,
345 pub production_number: Option<String>,
347 pub copyright: Option<String>,
349 pub creation_date: Option<String>,
351 pub production_company: Option<String>,
353 pub director: Option<String>,
355 pub producer: Option<String>,
357 pub additional: HashMap<String, String>,
359}
360
361impl ProductionMetadata {
362 #[must_use]
364 pub fn new() -> Self {
365 Self {
366 title: None,
367 episode_title: None,
368 series_title: None,
369 production_number: None,
370 copyright: None,
371 creation_date: None,
372 production_company: None,
373 director: None,
374 producer: None,
375 additional: HashMap::new(),
376 }
377 }
378
379 pub fn with_title(mut self, title: impl Into<String>) -> Self {
381 self.title = Some(title.into());
382 self
383 }
384
385 pub fn with_company(mut self, company: impl Into<String>) -> Self {
387 self.production_company = Some(company.into());
388 self
389 }
390
391 pub fn add_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
393 self.additional.insert(key.into(), value.into());
394 }
395}
396
397impl Default for ProductionMetadata {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403#[derive(Debug, Clone)]
405pub struct TechnicalMetadata {
406 pub video_format: Option<String>,
408 pub audio_format: Option<String>,
410 pub frame_rate: Option<EditRate>,
412 pub aspect_ratio: Option<String>,
414 pub resolution: Option<(u32, u32)>,
416 pub duration: Option<i64>,
418 pub file_size: Option<u64>,
420 pub codec: Option<String>,
422 pub additional: HashMap<String, String>,
424}
425
426impl TechnicalMetadata {
427 #[must_use]
429 pub fn new() -> Self {
430 Self {
431 video_format: None,
432 audio_format: None,
433 frame_rate: None,
434 aspect_ratio: None,
435 resolution: None,
436 duration: None,
437 file_size: None,
438 codec: None,
439 additional: HashMap::new(),
440 }
441 }
442
443 pub fn with_video_format(mut self, format: impl Into<String>) -> Self {
445 self.video_format = Some(format.into());
446 self
447 }
448
449 #[must_use]
451 pub fn with_frame_rate(mut self, rate: EditRate) -> Self {
452 self.frame_rate = Some(rate);
453 self
454 }
455
456 #[must_use]
458 pub fn with_resolution(mut self, width: u32, height: u32) -> Self {
459 self.resolution = Some((width, height));
460 self
461 }
462}
463
464impl Default for TechnicalMetadata {
465 fn default() -> Self {
466 Self::new()
467 }
468}
469
470#[derive(Debug, Clone, PartialEq, Eq)]
472pub enum MetadataError {
473 InvalidTimecode(String),
475 InvalidValue(String),
477 NotFound(String),
479}
480
481impl std::fmt::Display for MetadataError {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 match self {
484 MetadataError::InvalidTimecode(s) => write!(f, "Invalid timecode: {s}"),
485 MetadataError::InvalidValue(s) => write!(f, "Invalid metadata value: {s}"),
486 MetadataError::NotFound(s) => write!(f, "Metadata not found: {s}"),
487 }
488 }
489}
490
491impl std::error::Error for MetadataError {}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496
497 #[test]
498 fn test_comment() {
499 let comment = Comment::new("Author", "John Doe").with_category("Production");
500 assert_eq!(comment.name, "Author");
501 assert_eq!(comment.value, "John Doe");
502 assert_eq!(comment.category, Some("Production".to_string()));
503 }
504
505 #[test]
506 fn test_tagged_value() {
507 let tv_str = TaggedValue::string("Title", "My Video");
508 assert_eq!(tv_str.name, "Title");
509 if let TaggedValueData::String(s) = &tv_str.value {
510 assert_eq!(s, "My Video");
511 } else {
512 panic!("Expected string value");
513 }
514
515 let tv_int = TaggedValue::integer("FrameCount", 1000);
516 assert_eq!(tv_int.name, "FrameCount");
517 }
518
519 #[test]
520 fn test_klv_data() {
521 let key = vec![1, 2, 3, 4];
522 let value = vec![5, 6, 7, 8, 9];
523 let klv = KlvData::new(key.clone(), value.clone());
524
525 assert_eq!(klv.key(), &key);
526 assert_eq!(klv.value(), &value);
527 assert_eq!(klv.key_length(), 4);
528 assert_eq!(klv.value_length(), 5);
529 }
530
531 #[test]
532 fn test_timecode() {
533 let tc = Timecode::new(1, 2, 3, 4, 25, false);
534 assert_eq!(tc.hours, 1);
535 assert_eq!(tc.minutes, 2);
536 assert_eq!(tc.seconds, 3);
537 assert_eq!(tc.frames, 4);
538 assert_eq!(tc.to_string(), "01:02:03:04");
539 }
540
541 #[test]
542 fn test_timecode_parse() {
543 let tc = Timecode::parse("01:02:03:04", 25).expect("tc should be valid");
544 assert_eq!(tc.hours, 1);
545 assert_eq!(tc.minutes, 2);
546 assert_eq!(tc.seconds, 3);
547 assert_eq!(tc.frames, 4);
548 assert!(!tc.drop_frame);
549
550 let tc_df = Timecode::parse("01:02:03;04", 30).expect("tc_df should be valid");
551 assert!(tc_df.drop_frame);
552 }
553
554 #[test]
555 fn test_timecode_position_conversion() {
556 let edit_rate = EditRate::new(25, 1);
557 let tc = Timecode::new(0, 0, 1, 0, 25, false);
558 let pos = tc.to_position(edit_rate);
559 assert_eq!(pos.0, 25);
560
561 let tc2 = Timecode::from_position(pos, edit_rate);
562 assert_eq!(tc2.seconds, 1);
563 assert_eq!(tc2.frames, 0);
564 }
565
566 #[test]
567 fn test_descriptive_metadata() {
568 let mut dm = DescriptiveMetadata::new();
569 dm.add_item("title", MetadataValue::String("My Film".to_string()));
570 dm.add_item("year", MetadataValue::Integer(2024));
571
572 assert!(dm.get_item("title").is_some());
573 assert!(dm.get_item("year").is_some());
574 assert_eq!(dm.items().len(), 2);
575 }
576
577 #[test]
578 fn test_production_metadata() {
579 let pm = ProductionMetadata::new()
580 .with_title("Episode 1")
581 .with_company("ABC Productions");
582
583 assert_eq!(pm.title, Some("Episode 1".to_string()));
584 assert_eq!(pm.production_company, Some("ABC Productions".to_string()));
585 }
586
587 #[test]
588 fn test_technical_metadata() {
589 let tm = TechnicalMetadata::new()
590 .with_video_format("HD")
591 .with_frame_rate(EditRate::new(25, 1))
592 .with_resolution(1920, 1080);
593
594 assert_eq!(tm.video_format, Some("HD".to_string()));
595 assert_eq!(tm.resolution, Some((1920, 1080)));
596 }
597}