1use super::{EditType, Edl, EdlEvent, EdlResult, Timecode};
36use oximedia_core::Rational;
37use std::collections::HashMap;
38
39#[derive(Debug, Clone)]
41pub struct AleFile {
42 pub header: HashMap<String, String>,
44 pub columns: Vec<String>,
46 pub data: Vec<HashMap<String, String>>,
48}
49
50pub struct AleParser {
52 frame_rate: Rational,
53 audio_format: String,
54 video_format: String,
55}
56
57impl AleParser {
58 #[must_use]
60 pub fn new() -> Self {
61 Self {
62 frame_rate: Rational::new(24, 1),
63 audio_format: "48kHz".to_string(),
64 video_format: "1080p".to_string(),
65 }
66 }
67
68 pub fn parse(&mut self, content: &str) -> EdlResult<AleFile> {
70 let lines: Vec<&str> = content.lines().collect();
71 let mut header = HashMap::new();
72 let mut columns = Vec::new();
73 let mut data = Vec::new();
74
75 let mut section = Section::None;
76 let mut i = 0;
77
78 while i < lines.len() {
79 let line = lines[i].trim();
80
81 if line.is_empty() {
83 i += 1;
84 continue;
85 }
86
87 if line == "Heading" {
89 section = Section::Heading;
90 i += 1;
91 continue;
92 } else if line == "Column" {
93 section = Section::Column;
94 i += 1;
95 continue;
96 } else if line == "Data" {
97 section = Section::Data;
98 i += 1;
99 continue;
100 }
101
102 match section {
103 Section::None => {
104 i += 1;
106 }
107 Section::Heading => {
108 self.parse_header_line(line, &mut header)?;
109 i += 1;
110 }
111 Section::Column => {
112 columns = self.parse_column_line(line);
113 section = Section::Data;
114 i += 1;
115 }
116 Section::Data => {
117 if !columns.is_empty() {
118 let row = self.parse_data_line(line, &columns)?;
119 data.push(row);
120 }
121 i += 1;
122 }
123 }
124 }
125
126 if let Some(fps) = header.get("FPS") {
128 if let Ok(fps_val) = fps.parse::<i32>() {
129 self.frame_rate = Rational::new(i64::from(fps_val), 1);
130 }
131 }
132
133 if let Some(audio) = header.get("AUDIO_FORMAT") {
134 self.audio_format = audio.clone();
135 }
136
137 if let Some(video) = header.get("VIDEO_FORMAT") {
138 self.video_format = video.clone();
139 }
140
141 Ok(AleFile {
142 header,
143 columns,
144 data,
145 })
146 }
147
148 fn parse_header_line(&self, line: &str, header: &mut HashMap<String, String>) -> EdlResult<()> {
150 let parts: Vec<&str> = line.split('\t').collect();
151 if parts.len() >= 2 {
152 header.insert(parts[0].to_string(), parts[1].to_string());
153 }
154 Ok(())
155 }
156
157 fn parse_column_line(&self, line: &str) -> Vec<String> {
159 line.split('\t').map(|s| s.trim().to_string()).collect()
160 }
161
162 fn parse_data_line(
164 &self,
165 line: &str,
166 columns: &[String],
167 ) -> EdlResult<HashMap<String, String>> {
168 let values: Vec<&str> = line.split('\t').collect();
169 let mut row = HashMap::new();
170
171 for (i, column) in columns.iter().enumerate() {
172 if i < values.len() {
173 row.insert(column.clone(), values[i].trim().to_string());
174 }
175 }
176
177 Ok(row)
178 }
179
180 pub fn to_edl(&self, ale: &AleFile) -> EdlResult<Edl> {
182 let title = ale
183 .header
184 .get("TITLE")
185 .or_else(|| ale.header.get("PROJECT"))
186 .unwrap_or(&String::from("Untitled"))
187 .clone();
188
189 let mut edl = Edl::new(title, self.frame_rate, false);
190
191 for (key, value) in &ale.header {
193 edl.metadata.insert(key.clone(), value.clone());
194 }
195
196 for (idx, row) in ale.data.iter().enumerate() {
198 let event = self.row_to_event(row, idx + 1)?;
199 edl.add_event(event);
200 }
201
202 Ok(edl)
203 }
204
205 fn row_to_event(&self, row: &HashMap<String, String>, number: usize) -> EdlResult<EdlEvent> {
207 let name = row.get("Name").unwrap_or(&String::new()).clone();
209 let tape = row
210 .get("Tape")
211 .or_else(|| row.get("Source File"))
212 .unwrap_or(&String::new())
213 .clone();
214
215 let start = row.get("Start").or_else(|| row.get("Mark In"));
216 let end = row.get("End").or_else(|| row.get("Mark Out"));
217
218 let source_in = if let Some(tc) = start {
219 Timecode::parse(tc, self.frame_rate)?
220 } else {
221 Timecode::new(0, 0, 0, 0, false, self.frame_rate)
222 };
223
224 let source_out = if let Some(tc) = end {
225 Timecode::parse(tc, self.frame_rate)?
226 } else {
227 Timecode::new(0, 0, 0, 0, false, self.frame_rate)
228 };
229
230 let record_in = Timecode::from_frames((number as i64 - 1) * 150, self.frame_rate, false);
232 let duration = source_out.to_frames() - source_in.to_frames();
233 let record_out =
234 Timecode::from_frames(record_in.to_frames() + duration, self.frame_rate, false);
235
236 let track = if let Some(tracks) = row.get("Tracks") {
238 tracks.clone()
239 } else {
240 "V".to_string()
241 };
242
243 let mut metadata = HashMap::new();
245 for (key, value) in row {
246 metadata.insert(key.to_lowercase().replace(' ', "_"), value.clone());
247 }
248
249 if !name.is_empty() {
251 metadata.insert("clip_name".to_string(), name);
252 }
253
254 Ok(EdlEvent {
255 number: number as u32,
256 reel: tape,
257 track,
258 edit_type: EditType::Cut,
259 source_in,
260 source_out,
261 record_in,
262 record_out,
263 transition_duration: None,
264 motion_effect: None,
265 comments: Vec::new(),
266 metadata,
267 })
268 }
269}
270
271impl Default for AleParser {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277pub struct AleWriter {
279 columns: Vec<String>,
280 include_header: bool,
281}
282
283impl AleWriter {
284 #[must_use]
286 pub fn new() -> Self {
287 Self {
288 columns: vec![
289 "Name".to_string(),
290 "Tape".to_string(),
291 "Start".to_string(),
292 "End".to_string(),
293 "Duration".to_string(),
294 "Scene".to_string(),
295 "Take".to_string(),
296 ],
297 include_header: true,
298 }
299 }
300
301 #[must_use]
303 pub fn with_columns(mut self, columns: Vec<String>) -> Self {
304 self.columns = columns;
305 self
306 }
307
308 #[must_use]
310 pub fn with_header(mut self, include: bool) -> Self {
311 self.include_header = include;
312 self
313 }
314
315 pub fn write(&self, edl: &Edl) -> EdlResult<String> {
317 let mut output = String::new();
318
319 if self.include_header {
321 output.push_str("Heading\n");
322 output.push_str("FIELD_DELIM\tTABS\n");
323
324 let fps = edl.frame_rate.to_f64() as i32;
326 output.push_str(&format!("FPS\t{}\n", fps));
327
328 for (key, value) in &edl.metadata {
330 output.push_str(&format!("{}\t{}\n", key.to_uppercase(), value));
331 }
332
333 output.push('\n');
334 }
335
336 output.push_str("Column\n");
338 output.push_str(&self.columns.join("\t"));
339 output.push_str("\n\n");
340
341 output.push_str("Data\n");
343 for event in &edl.events {
344 self.write_event(&mut output, event);
345 }
346
347 Ok(output)
348 }
349
350 fn write_event(&self, output: &mut String, event: &EdlEvent) {
352 let mut values = Vec::new();
353
354 for column in &self.columns {
355 let value = match column.as_str() {
356 "Name" => event
357 .metadata
358 .get("clip_name")
359 .unwrap_or(&String::new())
360 .clone(),
361 "Tape" | "Source File" => event.reel.clone(),
362 "Start" | "Mark In" => event.source_in.format(),
363 "End" | "Mark Out" => event.source_out.format(),
364 "Duration" => {
365 let duration_frames =
366 event.source_out.to_frames() - event.source_in.to_frames();
367 Timecode::from_frames(
368 duration_frames,
369 event.source_in.frame_rate,
370 event.source_in.drop_frame,
371 )
372 .format()
373 }
374 "Scene" => event
375 .metadata
376 .get("scene")
377 .unwrap_or(&String::new())
378 .clone(),
379 "Take" => event.metadata.get("take").unwrap_or(&String::new()).clone(),
380 "Tracks" => event.track.clone(),
381 _ => event
382 .metadata
383 .get(&column.to_lowercase().replace(' ', "_"))
384 .unwrap_or(&String::new())
385 .clone(),
386 };
387
388 values.push(value);
389 }
390
391 output.push_str(&values.join("\t"));
392 output.push('\n');
393 }
394}
395
396impl Default for AleWriter {
397 fn default() -> Self {
398 Self::new()
399 }
400}
401
402#[derive(Debug, Clone, Copy, PartialEq, Eq)]
404enum Section {
405 None,
406 Heading,
407 Column,
408 Data,
409}
410
411pub fn parse(content: &str) -> EdlResult<Edl> {
413 let mut parser = AleParser::new();
414 let ale = parser.parse(content)?;
415 parser.to_edl(&ale)
416}
417
418pub fn write(edl: &Edl) -> EdlResult<String> {
420 AleWriter::new().write(edl)
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
428 fn test_parse_ale() {
429 let content = r"Heading
430FIELD_DELIM TABS
431FPS 24
432
433Column
434Name Tape Start End Duration
435
436Data
437CLIP001 A001 01:00:00:00 01:00:05:00 00:00:05:00
438CLIP002 A001 01:00:10:00 01:00:15:00 00:00:05:00
439";
440
441 let mut parser = AleParser::new();
442 let ale = parser.parse(content).expect("ale should be valid");
443
444 assert_eq!(ale.header.get("FPS"), Some(&"24".to_string()));
445 assert_eq!(ale.columns.len(), 5);
446 assert_eq!(ale.data.len(), 2);
447 assert_eq!(ale.data[0].get("Name"), Some(&"CLIP001".to_string()));
448 }
449
450 #[test]
451 fn test_ale_to_edl() {
452 let content = r"Heading
453FIELD_DELIM TABS
454FPS 24
455
456Column
457Name Tape Start End
458
459Data
460CLIP001 A001 01:00:00:00 01:00:05:00
461CLIP002 A001 01:00:10:00 01:00:15:00
462";
463
464 let edl = parse(content).expect("edl should be valid");
465 assert_eq!(edl.events.len(), 2);
466 assert_eq!(edl.events[0].reel, "A001");
467 assert_eq!(
468 edl.events[0].metadata.get("clip_name"),
469 Some(&"CLIP001".to_string())
470 );
471 }
472
473 #[test]
474 fn test_write_ale() {
475 let mut edl = Edl::new("Test Project".to_string(), Rational::new(24, 1), false);
476
477 let mut metadata = HashMap::new();
478 metadata.insert("clip_name".to_string(), "CLIP001".to_string());
479 metadata.insert("scene".to_string(), "1".to_string());
480 metadata.insert("take".to_string(), "1".to_string());
481
482 let event = EdlEvent {
483 number: 1,
484 reel: "A001".to_string(),
485 track: "V".to_string(),
486 edit_type: EditType::Cut,
487 source_in: Timecode::new(1, 0, 0, 0, false, Rational::new(24, 1)),
488 source_out: Timecode::new(1, 0, 5, 0, false, Rational::new(24, 1)),
489 record_in: Timecode::new(1, 0, 0, 0, false, Rational::new(24, 1)),
490 record_out: Timecode::new(1, 0, 5, 0, false, Rational::new(24, 1)),
491 transition_duration: None,
492 motion_effect: None,
493 comments: Vec::new(),
494 metadata,
495 };
496
497 edl.add_event(event);
498
499 let output = write(&edl).expect("output should be valid");
500 assert!(output.contains("Heading"));
501 assert!(output.contains("Column"));
502 assert!(output.contains("Data"));
503 assert!(output.contains("CLIP001"));
504 assert!(output.contains("A001"));
505 }
506
507 #[test]
508 fn test_ale_custom_columns() {
509 let mut edl = Edl::new("Test".to_string(), Rational::new(24, 1), false);
510
511 let mut metadata = HashMap::new();
512 metadata.insert("clip_name".to_string(), "CLIP001".to_string());
513 metadata.insert("camera".to_string(), "A".to_string());
514
515 let event = EdlEvent {
516 number: 1,
517 reel: "A001".to_string(),
518 track: "V".to_string(),
519 edit_type: EditType::Cut,
520 source_in: Timecode::new(1, 0, 0, 0, false, Rational::new(24, 1)),
521 source_out: Timecode::new(1, 0, 5, 0, false, Rational::new(24, 1)),
522 record_in: Timecode::new(1, 0, 0, 0, false, Rational::new(24, 1)),
523 record_out: Timecode::new(1, 0, 5, 0, false, Rational::new(24, 1)),
524 transition_duration: None,
525 motion_effect: None,
526 comments: Vec::new(),
527 metadata,
528 };
529
530 edl.add_event(event);
531
532 let writer = AleWriter::new().with_columns(vec![
533 "Name".to_string(),
534 "Tape".to_string(),
535 "Camera".to_string(),
536 ]);
537
538 let output = writer.write(&edl).expect("output should be valid");
539 assert!(output.contains("Camera"));
540 }
541}