1use crate::Result;
2use crate::core::formats::{
3 Subtitle, SubtitleEntry, SubtitleFormat, SubtitleFormatType, SubtitleMetadata,
4};
5use crate::error::SubXError;
6use regex::Regex;
7use std::time::Duration;
8
9pub struct SrtFormat;
11
12impl SubtitleFormat for SrtFormat {
13 fn parse(&self, content: &str) -> Result<Subtitle> {
14 let time_regex =
15 Regex::new(r"(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})")
16 .map_err(|e| {
17 SubXError::subtitle_format(
18 self.format_name(),
19 format!("Time format compilation error: {}", e),
20 )
21 })?;
22
23 let mut entries = Vec::new();
24 let blocks: Vec<&str> = content.split("\n\n").collect();
25
26 for block in blocks {
27 if block.trim().is_empty() {
28 continue;
29 }
30 let lines: Vec<&str> = block.lines().collect();
31 if lines.len() < 3 {
32 continue;
33 }
34
35 let index: usize = match lines[0].trim().parse() {
36 Ok(idx) => idx,
37 Err(e) => {
38 log::debug!(
39 "Skipping SRT block with invalid sequence number '{}': {}",
40 lines[0].trim(),
41 e
42 );
43 continue;
44 }
45 };
46
47 if let Some(caps) = time_regex.captures(lines[1]) {
48 let start_time = parse_time(&caps, 1)?;
49 let end_time = parse_time(&caps, 5)?;
50 let text = lines[2..].join("\n");
51
52 entries.push(SubtitleEntry {
53 index,
54 start_time,
55 end_time,
56 text,
57 styling: None,
58 });
59 }
60 }
61
62 Ok(Subtitle {
63 entries,
64 metadata: SubtitleMetadata {
65 title: None,
66 language: None,
67 encoding: "utf-8".to_string(),
68 frame_rate: None,
69 original_format: SubtitleFormatType::Srt,
70 },
71 format: SubtitleFormatType::Srt,
72 })
73 }
74
75 fn serialize(&self, subtitle: &Subtitle) -> Result<String> {
76 let mut output = String::new();
77
78 for (i, entry) in subtitle.entries.iter().enumerate() {
79 output.push_str(&format!("{}\n", i + 1));
80 output.push_str(&format_time_range(entry.start_time, entry.end_time));
81 output.push_str(&format!("{}\n\n", entry.text));
82 }
83
84 Ok(output)
85 }
86
87 fn detect(&self, content: &str) -> bool {
88 let time_pattern =
89 Regex::new(r"\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}").unwrap();
90 time_pattern.is_match(content)
91 }
92
93 fn format_name(&self) -> &'static str {
94 "SRT"
95 }
96
97 fn file_extensions(&self) -> &'static [&'static str] {
98 &["srt"]
99 }
100}
101
102fn parse_time(caps: ®ex::Captures, start_group: usize) -> Result<Duration> {
103 let hours: u64 = caps[start_group].parse().map_err(|e| {
104 SubXError::subtitle_format("SRT", format!("Time value parsing failed: {}", e))
105 })?;
106 let minutes: u64 = caps[start_group + 1].parse().map_err(|e| {
107 SubXError::subtitle_format("SRT", format!("Time value parsing failed: {}", e))
108 })?;
109 let seconds: u64 = caps[start_group + 2].parse().map_err(|e| {
110 SubXError::subtitle_format("SRT", format!("Time value parsing failed: {}", e))
111 })?;
112 let milliseconds: u64 = caps[start_group + 3].parse().map_err(|e| {
113 SubXError::subtitle_format("SRT", format!("Time value parsing failed: {}", e))
114 })?;
115
116 Ok(Duration::from_millis(
117 hours * 3600000 + minutes * 60000 + seconds * 1000 + milliseconds,
118 ))
119}
120
121fn format_time_range(start: Duration, end: Duration) -> String {
122 format!("{} --> {}\n", format_duration(start), format_duration(end))
123}
124
125fn format_duration(duration: Duration) -> String {
126 let total_ms = duration.as_millis();
127 let hours = total_ms / 3600000;
128 let minutes = (total_ms % 3600000) / 60000;
129 let seconds = (total_ms % 60000) / 1000;
130 let milliseconds = total_ms % 1000;
131
132 format!(
133 "{:02}:{:02}:{:02},{:03}",
134 hours, minutes, seconds, milliseconds
135 )
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::core::formats::{SubtitleFormat, SubtitleFormatType};
142 use std::time::Duration;
143
144 const SAMPLE_SRT: &str = "1\n00:00:01,000 --> 00:00:03,000\nHello, World!\n\n2\n00:00:05,000 --> 00:00:08,000\nThis is a test subtitle.\n多行測試\n\n";
146
147 #[test]
148 fn test_srt_parsing_basic() {
149 let format = SrtFormat;
150 let subtitle = format.parse(SAMPLE_SRT).unwrap();
151
152 assert_eq!(subtitle.entries.len(), 2);
153 assert_eq!(subtitle.format, SubtitleFormatType::Srt);
154
155 let first = &subtitle.entries[0];
156 assert_eq!(first.index, 1);
157 assert_eq!(first.start_time, Duration::from_millis(1000));
158 assert_eq!(first.end_time, Duration::from_millis(3000));
159 assert_eq!(first.text, "Hello, World!");
160
161 let second = &subtitle.entries[1];
162 assert_eq!(second.index, 2);
163 assert_eq!(second.start_time, Duration::from_millis(5000));
164 assert_eq!(second.end_time, Duration::from_millis(8000));
165 assert_eq!(second.text, "This is a test subtitle.\n多行測試");
166 }
167
168 #[test]
169 fn test_srt_serialization_roundtrip() {
170 let format = SrtFormat;
171 let subtitle = format.parse(SAMPLE_SRT).unwrap();
172 let serialized = format.serialize(&subtitle).unwrap();
173 let reparsed = format.parse(&serialized).unwrap();
174 assert_eq!(subtitle.entries.len(), reparsed.entries.len());
175 for (o, r) in subtitle.entries.iter().zip(reparsed.entries.iter()) {
176 assert_eq!(o.start_time, r.start_time);
177 assert_eq!(o.end_time, r.end_time);
178 assert_eq!(o.text, r.text);
179 }
180 }
181
182 #[test]
183 fn test_srt_detection() {
184 let format = SrtFormat;
185 assert!(format.detect(SAMPLE_SRT));
186 assert!(!format.detect("This is not SRT content"));
187 assert!(!format.detect("WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nHello"));
188 }
189
190 #[test]
191 fn test_srt_invalid_format() {
192 let format = SrtFormat;
193 let invalid_time = "1\n00:00:01 --> 00:00:03\nText\n\n";
194 let subtitle = format.parse(invalid_time).unwrap();
195 assert_eq!(subtitle.entries.len(), 0);
196 let invalid_index = "invalid\n00:00:01,000 --> 00:00:03,000\nText\n\n";
197 let subtitle = format.parse(invalid_index).unwrap();
198 assert_eq!(subtitle.entries.len(), 0);
199 }
200
201 #[test]
202 fn test_srt_empty_and_malformed_blocks() {
203 let format = SrtFormat;
204 let subtitle = format.parse("").unwrap();
205 assert_eq!(subtitle.entries.len(), 0);
206 let subtitle = format.parse("\n\n\n").unwrap();
207 assert_eq!(subtitle.entries.len(), 0);
208 let malformed = "1\n00:00:01,000 --> 00:00:03,000\n\n";
209 let subtitle = format.parse(malformed).unwrap();
210 assert_eq!(subtitle.entries.len(), 0);
211 }
212
213 #[test]
214 fn test_time_parsing_edge_cases() {
215 let format = SrtFormat;
216 let edge = "1\n23:59:59,999 --> 23:59:59,999\nEnd of day\n\n";
217 let subtitle = format.parse(edge).unwrap();
218 assert_eq!(subtitle.entries.len(), 1);
219 let entry = &subtitle.entries[0];
220 let expected = Duration::from_millis(23 * 3600000 + 59 * 60000 + 59 * 1000 + 999);
221 assert_eq!(entry.start_time, expected);
222 assert_eq!(entry.end_time, expected);
223 }
224
225 #[test]
226 fn test_file_extensions_and_name() {
227 let format = SrtFormat;
228 assert_eq!(format.file_extensions(), &["srt"]);
229 assert_eq!(format.format_name(), "SRT");
230 }
231
232 #[test]
233 fn test_srt_bad_block_index_skipped() {
234 let format = SrtFormat;
235 let input = "notanumber\n00:00:01,000 --> 00:00:02,000\nBad block\n\n\
236 2\n00:00:03,000 --> 00:00:04,000\nGood block\n\n";
237 let subtitle = format
238 .parse(input)
239 .expect("parser must not abort on bad block index");
240 assert_eq!(subtitle.entries.len(), 1);
241 assert_eq!(subtitle.entries[0].index, 2);
242 assert_eq!(subtitle.entries[0].text, "Good block");
243 }
244}