#![allow(dead_code)]
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum CommentType {
FromClipName,
ToClipName,
SourceFile,
EffectName,
Locator,
AscSop,
AscSat,
Speed,
Generic,
}
impl CommentType {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::FromClipName => "FROM CLIP NAME",
Self::ToClipName => "TO CLIP NAME",
Self::SourceFile => "SOURCE FILE",
Self::EffectName => "EFFECT NAME",
Self::Locator => "LOC",
Self::AscSop => "ASC_SOP",
Self::AscSat => "ASC_SAT",
Self::Speed => "SPEED",
Self::Generic => "COMMENT",
}
}
#[must_use]
pub const fn prefix(&self) -> &'static str {
match self {
Self::FromClipName => "* FROM CLIP NAME:",
Self::ToClipName => "* TO CLIP NAME:",
Self::SourceFile => "* SOURCE FILE:",
Self::EffectName => "* EFFECT NAME:",
Self::Locator => "* LOC:",
Self::AscSop => "* ASC_SOP",
Self::AscSat => "* ASC_SAT",
Self::Speed => ">>> SPEED:",
Self::Generic => "*",
}
}
#[must_use]
pub fn detect(line: &str) -> Option<Self> {
let trimmed = line.trim();
if trimmed.starts_with("* FROM CLIP NAME:") {
Some(Self::FromClipName)
} else if trimmed.starts_with("* TO CLIP NAME:") {
Some(Self::ToClipName)
} else if trimmed.starts_with("* SOURCE FILE:") {
Some(Self::SourceFile)
} else if trimmed.starts_with("* EFFECT NAME:") {
Some(Self::EffectName)
} else if trimmed.starts_with("* LOC:") {
Some(Self::Locator)
} else if trimmed.starts_with("* ASC_SOP") {
Some(Self::AscSop)
} else if trimmed.starts_with("* ASC_SAT") {
Some(Self::AscSat)
} else if trimmed.starts_with(">>> SPEED:") {
Some(Self::Speed)
} else if trimmed.starts_with('*') {
Some(Self::Generic)
} else {
None
}
}
}
impl fmt::Display for CommentType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EdlComment {
comment_type: CommentType,
value: String,
raw: String,
}
impl EdlComment {
#[must_use]
pub fn new(comment_type: CommentType, value: impl Into<String>) -> Self {
let value = value.into();
let raw = if comment_type == CommentType::Generic {
format!("* {value}")
} else {
format!("{} {value}", comment_type.prefix())
};
Self {
comment_type,
value,
raw,
}
}
#[must_use]
pub fn parse(line: &str) -> Option<Self> {
let ct = CommentType::detect(line)?;
let trimmed = line.trim();
let value = match ct {
CommentType::Generic => trimmed.strip_prefix('*').unwrap_or("").trim().to_string(),
_ => trimmed
.strip_prefix(ct.prefix())
.unwrap_or("")
.trim()
.to_string(),
};
Some(Self {
comment_type: ct,
value,
raw: line.to_string(),
})
}
#[must_use]
pub fn comment_type(&self) -> &CommentType {
&self.comment_type
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub fn to_edl_line(&self) -> String {
if self.comment_type == CommentType::Generic {
format!("* {}", self.value)
} else {
format!("{} {}", self.comment_type.prefix(), self.value)
}
}
}
impl fmt::Display for EdlComment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_edl_line())
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct CommentBlock {
comments: Vec<EdlComment>,
}
impl CommentBlock {
#[must_use]
pub fn new() -> Self {
Self {
comments: Vec::new(),
}
}
#[must_use]
pub fn parse(lines: &[&str]) -> Self {
let comments: Vec<EdlComment> = lines
.iter()
.filter_map(|line| EdlComment::parse(line))
.collect();
Self { comments }
}
pub fn push(&mut self, comment: EdlComment) {
self.comments.push(comment);
}
#[must_use]
pub fn len(&self) -> usize {
self.comments.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.comments.is_empty()
}
#[must_use]
pub fn of_type(&self, ct: &CommentType) -> Vec<&EdlComment> {
self.comments
.iter()
.filter(|c| &c.comment_type == ct)
.collect()
}
#[must_use]
pub fn from_clip_name(&self) -> Option<&str> {
self.of_type(&CommentType::FromClipName)
.first()
.map(|c| c.value.as_str())
}
#[must_use]
pub fn source_file(&self) -> Option<&str> {
self.of_type(&CommentType::SourceFile)
.first()
.map(|c| c.value.as_str())
}
pub fn iter(&self) -> impl Iterator<Item = &EdlComment> {
self.comments.iter()
}
#[must_use]
pub fn to_edl_string(&self) -> String {
self.comments
.iter()
.map(|c| c.to_edl_line())
.collect::<Vec<_>>()
.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_comment_type_detect_from_clip() {
let ct = CommentType::detect("* FROM CLIP NAME: shot001.mov");
assert_eq!(ct, Some(CommentType::FromClipName));
}
#[test]
fn test_comment_type_detect_to_clip() {
let ct = CommentType::detect("* TO CLIP NAME: shot002.mov");
assert_eq!(ct, Some(CommentType::ToClipName));
}
#[test]
fn test_comment_type_detect_source_file() {
let ct = CommentType::detect("* SOURCE FILE: /media/clip.mxf");
assert_eq!(ct, Some(CommentType::SourceFile));
}
#[test]
fn test_comment_type_detect_effect() {
let ct = CommentType::detect("* EFFECT NAME: CROSS DISSOLVE");
assert_eq!(ct, Some(CommentType::EffectName));
}
#[test]
fn test_comment_type_detect_speed() {
let ct = CommentType::detect(">>> SPEED: 050.0");
assert_eq!(ct, Some(CommentType::Speed));
}
#[test]
fn test_comment_type_detect_generic() {
let ct = CommentType::detect("* some random note");
assert_eq!(ct, Some(CommentType::Generic));
}
#[test]
fn test_comment_type_detect_none() {
let ct = CommentType::detect("001 AX V C");
assert!(ct.is_none());
}
#[test]
fn test_edl_comment_parse_from_clip() {
let c = EdlComment::parse("* FROM CLIP NAME: shot001.mov").expect("failed to parse");
assert_eq!(*c.comment_type(), CommentType::FromClipName);
assert_eq!(c.value(), "shot001.mov");
}
#[test]
fn test_edl_comment_parse_generic() {
let c = EdlComment::parse("* This is a note").expect("failed to parse");
assert_eq!(*c.comment_type(), CommentType::Generic);
assert_eq!(c.value(), "This is a note");
}
#[test]
fn test_edl_comment_to_edl_line() {
let c = EdlComment::new(CommentType::FromClipName, "shot001.mov");
assert_eq!(c.to_edl_line(), "* FROM CLIP NAME: shot001.mov");
}
#[test]
fn test_edl_comment_display() {
let c = EdlComment::new(CommentType::Generic, "hello");
assert_eq!(format!("{c}"), "* hello");
}
#[test]
fn test_comment_block_parse() {
let lines = vec![
"* FROM CLIP NAME: shot001.mov",
"* SOURCE FILE: /media/shot001.mov",
"001 AX V C ...",
"* some note",
];
let block = CommentBlock::parse(&lines);
assert_eq!(block.len(), 3);
}
#[test]
fn test_comment_block_from_clip_name() {
let mut block = CommentBlock::new();
block.push(EdlComment::new(CommentType::FromClipName, "interview.mov"));
assert_eq!(block.from_clip_name(), Some("interview.mov"));
}
#[test]
fn test_comment_block_source_file() {
let mut block = CommentBlock::new();
block.push(EdlComment::new(CommentType::SourceFile, "/media/clip.mxf"));
assert_eq!(block.source_file(), Some("/media/clip.mxf"));
}
#[test]
fn test_comment_block_of_type() {
let mut block = CommentBlock::new();
block.push(EdlComment::new(CommentType::Generic, "note 1"));
block.push(EdlComment::new(CommentType::FromClipName, "clip.mov"));
block.push(EdlComment::new(CommentType::Generic, "note 2"));
assert_eq!(block.of_type(&CommentType::Generic).len(), 2);
assert_eq!(block.of_type(&CommentType::FromClipName).len(), 1);
}
#[test]
fn test_comment_block_to_edl_string() {
let mut block = CommentBlock::new();
block.push(EdlComment::new(CommentType::FromClipName, "clip.mov"));
block.push(EdlComment::new(CommentType::Generic, "note"));
let s = block.to_edl_string();
assert!(s.contains("* FROM CLIP NAME: clip.mov"));
assert!(s.contains("* note"));
}
#[test]
fn test_comment_type_label() {
assert_eq!(CommentType::FromClipName.label(), "FROM CLIP NAME");
assert_eq!(CommentType::Speed.label(), "SPEED");
assert_eq!(CommentType::Generic.label(), "COMMENT");
}
}