1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
8pub enum FrameRate {
9 Fps23976,
11 Fps24,
13 Fps25,
15 Fps2997DF,
17 Fps2997NDF,
19 Fps30,
21 Fps50,
23 Fps5994,
25 Fps60,
27 Custom(f64),
29}
30
31impl FrameRate {
32 #[must_use]
34 pub fn as_f64(self) -> f64 {
35 match self {
36 Self::Fps23976 => 24000.0 / 1001.0,
37 Self::Fps24 => 24.0,
38 Self::Fps25 => 25.0,
39 Self::Fps2997DF | Self::Fps2997NDF => 30000.0 / 1001.0,
40 Self::Fps30 => 30.0,
41 Self::Fps50 => 50.0,
42 Self::Fps5994 => 60000.0 / 1001.0,
43 Self::Fps60 => 60.0,
44 Self::Custom(fps) => fps,
45 }
46 }
47
48 #[must_use]
50 pub const fn as_rational(self) -> (u32, u32) {
51 match self {
52 Self::Fps23976 => (24000, 1001),
53 Self::Fps24 => (24, 1),
54 Self::Fps25 => (25, 1),
55 Self::Fps2997DF | Self::Fps2997NDF => (30000, 1001),
56 Self::Fps30 => (30, 1),
57 Self::Fps50 => (50, 1),
58 Self::Fps5994 => (60000, 1001),
59 Self::Fps60 => (60, 1),
60 Self::Custom(_) => (0, 0), }
62 }
63
64 #[must_use]
66 pub const fn is_drop_frame(self) -> bool {
67 matches!(self, Self::Fps2997DF)
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
73pub struct Timecode {
74 pub hours: u8,
76 pub minutes: u8,
78 pub seconds: u8,
80 pub frames: u8,
82}
83
84impl Timecode {
85 #[must_use]
87 pub const fn new(hours: u8, minutes: u8, seconds: u8, frames: u8) -> Self {
88 Self {
89 hours,
90 minutes,
91 seconds,
92 frames,
93 }
94 }
95
96 #[must_use]
98 pub fn to_frames(self, fps: FrameRate) -> u64 {
99 let fps_int = fps.as_f64() as u64;
100 let hours_frames = u64::from(self.hours) * 3600 * fps_int;
101 let minutes_frames = u64::from(self.minutes) * 60 * fps_int;
102 let seconds_frames = u64::from(self.seconds) * fps_int;
103 hours_frames + minutes_frames + seconds_frames + u64::from(self.frames)
104 }
105
106 #[must_use]
108 pub fn from_frames(frames: u64, fps: FrameRate) -> Self {
109 let fps_int = fps.as_f64() as u64;
110 let hours = (frames / (3600 * fps_int)) as u8;
111 let remaining = frames % (3600 * fps_int);
112 let minutes = (remaining / (60 * fps_int)) as u8;
113 let remaining = remaining % (60 * fps_int);
114 let seconds = (remaining / fps_int) as u8;
115 let frames = (remaining % fps_int) as u8;
116 Self::new(hours, minutes, seconds, frames)
117 }
118
119 pub fn parse(s: &str) -> Result<Self, String> {
125 let parts: Vec<&str> = s.split(':').collect();
126 if parts.len() != 4 {
127 return Err(format!("Invalid timecode format: {s}"));
128 }
129
130 let hours = parts[0]
131 .parse::<u8>()
132 .map_err(|_| format!("Invalid hours: {}", parts[0]))?;
133 let minutes = parts[1]
134 .parse::<u8>()
135 .map_err(|_| format!("Invalid minutes: {}", parts[1]))?;
136 let seconds = parts[2]
137 .parse::<u8>()
138 .map_err(|_| format!("Invalid seconds: {}", parts[2]))?;
139 let frames = parts[3]
140 .parse::<u8>()
141 .map_err(|_| format!("Invalid frames: {}", parts[3]))?;
142
143 Ok(Self::new(hours, minutes, seconds, frames))
144 }
145}
146
147impl std::fmt::Display for Timecode {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(
150 f,
151 "{:02}:{:02}:{:02}:{:02}",
152 self.hours, self.minutes, self.seconds, self.frames
153 )
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct MediaFile {
160 pub id: uuid::Uuid,
162 pub path: PathBuf,
164 pub filename: String,
166 pub duration: Option<f64>,
168 pub timecode_start: Option<Timecode>,
170 pub width: Option<u32>,
172 pub height: Option<u32>,
174 pub fps: Option<FrameRate>,
176 pub size: Option<u64>,
178 pub md5: Option<String>,
180 pub xxhash: Option<String>,
182 pub metadata: Option<String>,
184 pub cataloged_at: chrono::DateTime<chrono::Utc>,
186}
187
188impl MediaFile {
189 #[must_use]
191 pub fn new(path: PathBuf) -> Self {
192 let filename = path
193 .file_name()
194 .and_then(|n| n.to_str())
195 .unwrap_or("")
196 .to_string();
197
198 Self {
199 id: uuid::Uuid::new_v4(),
200 path,
201 filename,
202 duration: None,
203 timecode_start: None,
204 width: None,
205 height: None,
206 fps: None,
207 size: None,
208 md5: None,
209 xxhash: None,
210 metadata: None,
211 cataloged_at: chrono::Utc::now(),
212 }
213 }
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct ClipReference {
219 pub id: String,
221 pub source_file: Option<String>,
223 pub source_in: Timecode,
225 pub source_out: Timecode,
227 pub record_in: Timecode,
229 pub record_out: Timecode,
231 pub track: TrackType,
233 pub fps: FrameRate,
235 pub metadata: std::collections::HashMap<String, String>,
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
241pub enum TrackType {
242 Video,
244 Audio,
246 AudioVideo,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ClipMatch {
253 pub clip: ClipReference,
255 pub media: MediaFile,
257 pub score: f64,
259 pub method: MatchMethod,
261 pub details: String,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
267pub enum MatchMethod {
268 ExactFilename,
270 FuzzyFilename,
272 Timecode,
274 ContentHash,
276 Duration,
278 Combined,
280}
281
282impl std::fmt::Display for MatchMethod {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 let s = match self {
285 Self::ExactFilename => "Exact Filename",
286 Self::FuzzyFilename => "Fuzzy Filename",
287 Self::Timecode => "Timecode",
288 Self::ContentHash => "Content Hash",
289 Self::Duration => "Duration",
290 Self::Combined => "Combined",
291 };
292 write!(f, "{s}")
293 }
294}
295
296#[derive(Debug, Clone, Default, Serialize, Deserialize)]
298pub struct ConformClip {
299 pub name: String,
301 pub offset_s: f64,
303 pub duration_s: f64,
305 pub src_in_s: f64,
307 pub src_out_s: f64,
309}
310
311#[derive(Debug, Clone, Default, Serialize, Deserialize)]
313pub struct ConformSequence {
314 pub name: String,
316 pub frame_rate: f32,
318 pub clips: Vec<ConformClip>,
320}
321
322#[derive(Debug, Clone, Default, Serialize, Deserialize)]
324pub struct ConformProject {
325 pub name: String,
327 pub sequences: Vec<ConformSequence>,
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
333pub enum OutputFormat {
334 Mp4,
336 Matroska,
338 FrameSequenceDpx,
340 FrameSequenceTiff,
342 FrameSequencePng,
344 Edl,
346 FcpXml,
348 PremiereXml,
350 Aaf,
352}
353
354impl std::fmt::Display for OutputFormat {
355 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356 let s = match self {
357 Self::Mp4 => "MP4",
358 Self::Matroska => "Matroska",
359 Self::FrameSequenceDpx => "DPX Sequence",
360 Self::FrameSequenceTiff => "TIFF Sequence",
361 Self::FrameSequencePng => "PNG Sequence",
362 Self::Edl => "EDL",
363 Self::FcpXml => "FCP XML",
364 Self::PremiereXml => "Premiere XML",
365 Self::Aaf => "AAF",
366 };
367 write!(f, "{s}")
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn test_frame_rate_conversion() {
377 assert!((FrameRate::Fps25.as_f64() - 25.0).abs() < f64::EPSILON);
378 assert!((FrameRate::Fps2997NDF.as_f64() - 29.97).abs() < 0.01);
379 }
380
381 #[test]
382 fn test_timecode_frames_conversion() {
383 let tc = Timecode::new(1, 0, 0, 0);
384 let frames = tc.to_frames(FrameRate::Fps25);
385 assert_eq!(frames, 90000); let tc2 = Timecode::from_frames(frames, FrameRate::Fps25);
388 assert_eq!(tc, tc2);
389 }
390
391 #[test]
392 fn test_timecode_parsing() {
393 let tc = Timecode::parse("01:23:45:12").expect("tc should be valid");
394 assert_eq!(tc.hours, 1);
395 assert_eq!(tc.minutes, 23);
396 assert_eq!(tc.seconds, 45);
397 assert_eq!(tc.frames, 12);
398 }
399
400 #[test]
401 fn test_timecode_display() {
402 let tc = Timecode::new(1, 23, 45, 12);
403 assert_eq!(tc.to_string(), "01:23:45:12");
404 }
405
406 #[test]
407 fn test_timecode_comparison() {
408 let tc1 = Timecode::new(1, 0, 0, 0);
409 let tc2 = Timecode::new(1, 0, 0, 1);
410 assert!(tc1 < tc2);
411 }
412}