use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum FrameRate {
Fps23976,
Fps24,
Fps25,
Fps2997DF,
Fps2997NDF,
Fps30,
Fps50,
Fps5994,
Fps60,
Custom(f64),
}
impl FrameRate {
#[must_use]
pub fn as_f64(self) -> f64 {
match self {
Self::Fps23976 => 24000.0 / 1001.0,
Self::Fps24 => 24.0,
Self::Fps25 => 25.0,
Self::Fps2997DF | Self::Fps2997NDF => 30000.0 / 1001.0,
Self::Fps30 => 30.0,
Self::Fps50 => 50.0,
Self::Fps5994 => 60000.0 / 1001.0,
Self::Fps60 => 60.0,
Self::Custom(fps) => fps,
}
}
#[must_use]
pub const fn as_rational(self) -> (u32, u32) {
match self {
Self::Fps23976 => (24000, 1001),
Self::Fps24 => (24, 1),
Self::Fps25 => (25, 1),
Self::Fps2997DF | Self::Fps2997NDF => (30000, 1001),
Self::Fps30 => (30, 1),
Self::Fps50 => (50, 1),
Self::Fps5994 => (60000, 1001),
Self::Fps60 => (60, 1),
Self::Custom(_) => (0, 0), }
}
#[must_use]
pub const fn is_drop_frame(self) -> bool {
matches!(self, Self::Fps2997DF)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Timecode {
pub hours: u8,
pub minutes: u8,
pub seconds: u8,
pub frames: u8,
}
impl Timecode {
#[must_use]
pub const fn new(hours: u8, minutes: u8, seconds: u8, frames: u8) -> Self {
Self {
hours,
minutes,
seconds,
frames,
}
}
#[must_use]
pub fn to_frames(self, fps: FrameRate) -> u64 {
let fps_int = fps.as_f64() as u64;
let hours_frames = u64::from(self.hours) * 3600 * fps_int;
let minutes_frames = u64::from(self.minutes) * 60 * fps_int;
let seconds_frames = u64::from(self.seconds) * fps_int;
hours_frames + minutes_frames + seconds_frames + u64::from(self.frames)
}
#[must_use]
pub fn from_frames(frames: u64, fps: FrameRate) -> Self {
let fps_int = fps.as_f64() as u64;
let hours = (frames / (3600 * fps_int)) as u8;
let remaining = frames % (3600 * fps_int);
let minutes = (remaining / (60 * fps_int)) as u8;
let remaining = remaining % (60 * fps_int);
let seconds = (remaining / fps_int) as u8;
let frames = (remaining % fps_int) as u8;
Self::new(hours, minutes, seconds, frames)
}
pub fn parse(s: &str) -> Result<Self, String> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 4 {
return Err(format!("Invalid timecode format: {s}"));
}
let hours = parts[0]
.parse::<u8>()
.map_err(|_| format!("Invalid hours: {}", parts[0]))?;
let minutes = parts[1]
.parse::<u8>()
.map_err(|_| format!("Invalid minutes: {}", parts[1]))?;
let seconds = parts[2]
.parse::<u8>()
.map_err(|_| format!("Invalid seconds: {}", parts[2]))?;
let frames = parts[3]
.parse::<u8>()
.map_err(|_| format!("Invalid frames: {}", parts[3]))?;
Ok(Self::new(hours, minutes, seconds, frames))
}
}
impl std::fmt::Display for Timecode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:02}:{:02}:{:02}:{:02}",
self.hours, self.minutes, self.seconds, self.frames
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaFile {
pub id: uuid::Uuid,
pub path: PathBuf,
pub filename: String,
pub duration: Option<f64>,
pub timecode_start: Option<Timecode>,
pub width: Option<u32>,
pub height: Option<u32>,
pub fps: Option<FrameRate>,
pub size: Option<u64>,
pub md5: Option<String>,
pub xxhash: Option<String>,
pub metadata: Option<String>,
pub cataloged_at: chrono::DateTime<chrono::Utc>,
}
impl MediaFile {
#[must_use]
pub fn new(path: PathBuf) -> Self {
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
Self {
id: uuid::Uuid::new_v4(),
path,
filename,
duration: None,
timecode_start: None,
width: None,
height: None,
fps: None,
size: None,
md5: None,
xxhash: None,
metadata: None,
cataloged_at: chrono::Utc::now(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClipReference {
pub id: String,
pub source_file: Option<String>,
pub source_in: Timecode,
pub source_out: Timecode,
pub record_in: Timecode,
pub record_out: Timecode,
pub track: TrackType,
pub fps: FrameRate,
pub metadata: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TrackType {
Video,
Audio,
AudioVideo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClipMatch {
pub clip: ClipReference,
pub media: MediaFile,
pub score: f64,
pub method: MatchMethod,
pub details: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MatchMethod {
ExactFilename,
FuzzyFilename,
Timecode,
ContentHash,
Duration,
Combined,
}
impl std::fmt::Display for MatchMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::ExactFilename => "Exact Filename",
Self::FuzzyFilename => "Fuzzy Filename",
Self::Timecode => "Timecode",
Self::ContentHash => "Content Hash",
Self::Duration => "Duration",
Self::Combined => "Combined",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConformClip {
pub name: String,
pub offset_s: f64,
pub duration_s: f64,
pub src_in_s: f64,
pub src_out_s: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConformSequence {
pub name: String,
pub frame_rate: f32,
pub clips: Vec<ConformClip>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConformProject {
pub name: String,
pub sequences: Vec<ConformSequence>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OutputFormat {
Mp4,
Matroska,
FrameSequenceDpx,
FrameSequenceTiff,
FrameSequencePng,
Edl,
FcpXml,
PremiereXml,
Aaf,
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Mp4 => "MP4",
Self::Matroska => "Matroska",
Self::FrameSequenceDpx => "DPX Sequence",
Self::FrameSequenceTiff => "TIFF Sequence",
Self::FrameSequencePng => "PNG Sequence",
Self::Edl => "EDL",
Self::FcpXml => "FCP XML",
Self::PremiereXml => "Premiere XML",
Self::Aaf => "AAF",
};
write!(f, "{s}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frame_rate_conversion() {
assert!((FrameRate::Fps25.as_f64() - 25.0).abs() < f64::EPSILON);
assert!((FrameRate::Fps2997NDF.as_f64() - 29.97).abs() < 0.01);
}
#[test]
fn test_timecode_frames_conversion() {
let tc = Timecode::new(1, 0, 0, 0);
let frames = tc.to_frames(FrameRate::Fps25);
assert_eq!(frames, 90000);
let tc2 = Timecode::from_frames(frames, FrameRate::Fps25);
assert_eq!(tc, tc2);
}
#[test]
fn test_timecode_parsing() {
let tc = Timecode::parse("01:23:45:12").expect("tc should be valid");
assert_eq!(tc.hours, 1);
assert_eq!(tc.minutes, 23);
assert_eq!(tc.seconds, 45);
assert_eq!(tc.frames, 12);
}
#[test]
fn test_timecode_display() {
let tc = Timecode::new(1, 23, 45, 12);
assert_eq!(tc.to_string(), "01:23:45:12");
}
#[test]
fn test_timecode_comparison() {
let tc1 = Timecode::new(1, 0, 0, 0);
let tc2 = Timecode::new(1, 0, 0, 1);
assert!(tc1 < tc2);
}
}