subx_cli/core/formats/
sub.rs1use crate::Result;
16use crate::core::formats::{
17 Subtitle, SubtitleEntry, SubtitleFormat, SubtitleFormatType, SubtitleMetadata,
18};
19use crate::error::SubXError;
20use regex::Regex;
21use std::time::Duration;
22
23const DEFAULT_SUB_FPS: f32 = 25.0;
24
25pub struct SubFormat;
30
31impl SubtitleFormat for SubFormat {
32 fn parse(&self, content: &str) -> Result<Subtitle> {
33 let fps = DEFAULT_SUB_FPS;
34 let re = Regex::new(r"^\{(\d+)\}\{(\d+)\}(.*)").map_err(|e: regex::Error| {
35 SubXError::subtitle_format(self.format_name(), e.to_string())
36 })?;
37 let mut entries = Vec::new();
38 for line in content.lines() {
39 let l = line.trim();
40 if l.is_empty() {
41 continue;
42 }
43 if let Some(cap) = re.captures(l) {
44 let start_frame: u64 = cap[1].parse().map_err(|e: std::num::ParseIntError| {
45 SubXError::subtitle_format(self.format_name(), e.to_string())
46 })?;
47 let end_frame: u64 = cap[2].parse().map_err(|e: std::num::ParseIntError| {
48 SubXError::subtitle_format(self.format_name(), e.to_string())
49 })?;
50 let text = cap[3].replace("|", "\n");
51 let start_time = Duration::from_millis(
52 (start_frame as f64 * 1000.0 / fps as f64).round() as u64,
53 );
54 let end_time =
55 Duration::from_millis((end_frame as f64 * 1000.0 / fps as f64).round() as u64);
56 entries.push(SubtitleEntry {
57 index: entries.len() + 1,
58 start_time,
59 end_time,
60 text,
61 styling: None,
62 });
63 }
64 }
65 Ok(Subtitle {
66 entries,
67 metadata: SubtitleMetadata {
68 title: None,
69 language: None,
70 encoding: "utf-8".to_string(),
71 frame_rate: Some(fps),
72 original_format: SubtitleFormatType::Sub,
73 },
74 format: SubtitleFormatType::Sub,
75 })
76 }
77
78 fn serialize(&self, subtitle: &Subtitle) -> Result<String> {
79 let fps = subtitle.metadata.frame_rate.unwrap_or(DEFAULT_SUB_FPS);
80 let mut output = String::new();
81 for entry in &subtitle.entries {
82 let start_frame = (entry.start_time.as_secs_f64() * fps as f64).round() as u64;
83 let end_frame = (entry.end_time.as_secs_f64() * fps as f64).round() as u64;
84 let text = entry.text.replace("\n", "|");
85 output.push_str(&format!("{{{}}}{{{}}}{}\n", start_frame, end_frame, text));
86 }
87 Ok(output)
88 }
89
90 fn detect(&self, content: &str) -> bool {
91 if let Ok(re) = Regex::new(r"^\{\d+\}\{\d+\}") {
92 return re.is_match(content.trim_start());
93 }
94 false
95 }
96
97 fn format_name(&self) -> &'static str {
98 "SUB"
99 }
100
101 fn file_extensions(&self) -> &'static [&'static str] {
102 &["sub"]
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 const SAMPLE: &str = "{10}{20}Hello|World\n";
111
112 #[test]
113 fn test_parse_and_serialize() {
114 let fmt = SubFormat;
115 let subtitle = fmt.parse(SAMPLE).expect("parse failed");
116 assert_eq!(subtitle.entries.len(), 1);
117 let out = fmt.serialize(&subtitle).expect("serialize failed");
118 assert!(out.contains("{10}{20}Hello|World"));
119 }
120
121 #[test]
122 fn test_detect_true_and_false() {
123 let fmt = SubFormat;
124 assert!(fmt.detect(SAMPLE));
125 assert!(!fmt.detect("random text"));
126 }
127
128 #[test]
129 fn test_parse_multiple_and_frame_rate() {
130 let custom = "{0}{25}First|Line\n{25}{50}Second|Line\n";
131 let fmt = SubFormat;
132 let subtitle = fmt.parse(custom).expect("parse multiple failed");
133 assert_eq!(subtitle.entries.len(), 2);
134 assert_eq!(subtitle.metadata.frame_rate, Some(25.0));
135 assert_eq!(subtitle.entries[0].text, "First\nLine");
136 assert_eq!(subtitle.entries[1].text, "Second\nLine");
137 }
138
139 #[test]
140 fn test_serialize_with_nondefault_fps() {
141 let mut subtitle = Subtitle {
142 entries: Vec::new(),
143 metadata: SubtitleMetadata {
144 title: None,
145 language: None,
146 encoding: "utf-8".to_string(),
147 frame_rate: Some(50.0),
148 original_format: SubtitleFormatType::Sub,
149 },
150 format: SubtitleFormatType::Sub,
151 };
152 subtitle.entries.push(SubtitleEntry {
153 index: 1,
154 start_time: Duration::from_secs_f64(1.0),
155 end_time: Duration::from_secs_f64(2.0),
156 text: "X".into(),
157 styling: None,
158 });
159 let fmt = SubFormat;
160 let out = fmt.serialize(&subtitle).expect("serialize fps failed");
161 assert!(out.contains("{50}{100}X"));
163 }
164}