oximedia_proxy/conform/
timeline.rs1use crate::Result;
4use std::path::PathBuf;
5
6pub struct TimelineConformer {
8 original_dir: Option<PathBuf>,
10}
11
12impl TimelineConformer {
13 #[must_use]
15 pub const fn new() -> Self {
16 Self { original_dir: None }
17 }
18
19 #[must_use]
21 pub fn with_original_dir(mut self, dir: PathBuf) -> Self {
22 self.original_dir = Some(dir);
23 self
24 }
25
26 pub fn conform_timeline(
28 &self,
29 timeline_path: &std::path::Path,
30 output_path: &std::path::Path,
31 ) -> Result<TimelineConformResult> {
32 Ok(TimelineConformResult {
34 input_path: timeline_path.to_path_buf(),
35 output_path: output_path.to_path_buf(),
36 clips_relinked: 0,
37 clips_failed: 0,
38 clips_total: 0,
39 })
40 }
41
42 pub fn extract_media_references(
44 &self,
45 timeline_path: &std::path::Path,
46 ) -> Result<Vec<MediaReference>> {
47 let _timeline_path = timeline_path;
49 Ok(Vec::new())
50 }
51
52 pub fn validate_timeline(&self, timeline_path: &std::path::Path) -> Result<TimelineValidation> {
54 let references = self.extract_media_references(timeline_path)?;
55 let total = references.len();
56 let mut found = 0;
57 let mut missing = Vec::new();
58
59 for reference in &references {
60 if reference.path.exists() {
61 found += 1;
62 } else {
63 missing.push(reference.clone());
64 }
65 }
66
67 Ok(TimelineValidation {
68 total_references: total,
69 found_references: found,
70 missing_references: missing,
71 })
72 }
73}
74
75impl Default for TimelineConformer {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct TimelineConformResult {
84 pub input_path: PathBuf,
86
87 pub output_path: PathBuf,
89
90 pub clips_relinked: usize,
92
93 pub clips_failed: usize,
95
96 pub clips_total: usize,
98}
99
100impl TimelineConformResult {
101 #[must_use]
103 pub const fn is_success(&self) -> bool {
104 self.clips_failed == 0
105 }
106
107 #[must_use]
109 pub fn success_percentage(&self) -> f64 {
110 if self.clips_total == 0 {
111 100.0
112 } else {
113 (self.clips_relinked as f64 / self.clips_total as f64) * 100.0
114 }
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct MediaReference {
121 pub path: PathBuf,
123
124 pub clip_name: String,
126
127 pub in_point: u64,
129
130 pub out_point: u64,
132
133 pub track: usize,
135
136 pub is_video: bool,
138
139 pub is_audio: bool,
141}
142
143#[derive(Debug, Clone)]
145pub struct TimelineValidation {
146 pub total_references: usize,
148
149 pub found_references: usize,
151
152 pub missing_references: Vec<MediaReference>,
154}
155
156impl TimelineValidation {
157 #[must_use]
159 pub const fn is_valid(&self) -> bool {
160 self.missing_references.is_empty()
161 }
162
163 #[must_use]
165 pub fn validation_percentage(&self) -> f64 {
166 if self.total_references == 0 {
167 100.0
168 } else {
169 (self.found_references as f64 / self.total_references as f64) * 100.0
170 }
171 }
172}
173
174pub struct TimelineFormatDetector;
176
177impl TimelineFormatDetector {
178 pub fn detect(path: &std::path::Path) -> Result<TimelineFormat> {
184 if let Some(ext) = path.extension() {
186 match ext.to_str() {
187 Some("fcpxml") => return Ok(TimelineFormat::FinalCutProXml),
188 Some("xml") => {
189 return Ok(TimelineFormat::PremiereXml);
192 }
193 Some("aaf") => return Ok(TimelineFormat::Aaf),
194 Some("edl") => return Ok(TimelineFormat::Edl),
195 Some("otio") => return Ok(TimelineFormat::Otio),
196 _ => {}
197 }
198 }
199
200 Ok(TimelineFormat::Unknown)
201 }
202
203 #[must_use]
205 pub fn is_supported(format: &TimelineFormat) -> bool {
206 !matches!(format, TimelineFormat::Unknown)
207 }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum TimelineFormat {
213 FinalCutProXml,
215
216 PremiereXml,
218
219 Aaf,
221
222 Edl,
224
225 Otio,
227
228 Unknown,
230}
231
232impl TimelineFormat {
233 #[must_use]
235 pub const fn name(&self) -> &'static str {
236 match self {
237 Self::FinalCutProXml => "Final Cut Pro XML",
238 Self::PremiereXml => "Premiere Pro XML",
239 Self::Aaf => "AAF",
240 Self::Edl => "EDL",
241 Self::Otio => "OpenTimelineIO",
242 Self::Unknown => "Unknown",
243 }
244 }
245
246 #[must_use]
248 pub const fn extension(&self) -> &'static str {
249 match self {
250 Self::FinalCutProXml => "fcpxml",
251 Self::PremiereXml => "xml",
252 Self::Aaf => "aaf",
253 Self::Edl => "edl",
254 Self::Otio => "otio",
255 Self::Unknown => "",
256 }
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_timeline_conformer() {
266 let conformer = TimelineConformer::new();
267 let result = conformer.conform_timeline(
268 std::path::Path::new("timeline.xml"),
269 std::path::Path::new("output.xml"),
270 );
271 assert!(result.is_ok());
272 }
273
274 #[test]
275 fn test_timeline_conform_result() {
276 let result = TimelineConformResult {
277 input_path: PathBuf::from("input.xml"),
278 output_path: PathBuf::from("output.xml"),
279 clips_relinked: 8,
280 clips_failed: 2,
281 clips_total: 10,
282 };
283
284 assert!(!result.is_success());
285 assert_eq!(result.success_percentage(), 80.0);
286 }
287
288 #[test]
289 fn test_format_detector() {
290 let format = TimelineFormatDetector::detect(std::path::Path::new("test.fcpxml"));
291 assert!(format.is_ok());
292 assert_eq!(
293 format.expect("should succeed in test"),
294 TimelineFormat::FinalCutProXml
295 );
296
297 let format = TimelineFormatDetector::detect(std::path::Path::new("test.edl"));
298 assert!(format.is_ok());
299 assert_eq!(format.expect("should succeed in test"), TimelineFormat::Edl);
300 }
301
302 #[test]
303 fn test_format_name() {
304 assert_eq!(TimelineFormat::FinalCutProXml.name(), "Final Cut Pro XML");
305 assert_eq!(TimelineFormat::Aaf.name(), "AAF");
306 }
307
308 #[test]
309 fn test_is_supported() {
310 assert!(TimelineFormatDetector::is_supported(
311 &TimelineFormat::FinalCutProXml
312 ));
313 assert!(!TimelineFormatDetector::is_supported(
314 &TimelineFormat::Unknown
315 ));
316 }
317}