subx_cli/core/formats/
srt.rs

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
9/// SubRip (.srt) 格式解析與序列化
10pub 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!("時間格式編譯錯誤: {}", 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 = lines[0].trim().parse().map_err(|e| {
36                SubXError::subtitle_format(self.format_name(), format!("無效的序列號: {}", e))
37            })?;
38
39            if let Some(caps) = time_regex.captures(lines[1]) {
40                let start_time = parse_time(&caps, 1)?;
41                let end_time = parse_time(&caps, 5)?;
42                let text = lines[2..].join("\n");
43
44                entries.push(SubtitleEntry {
45                    index,
46                    start_time,
47                    end_time,
48                    text,
49                    styling: None,
50                });
51            }
52        }
53
54        Ok(Subtitle {
55            entries,
56            metadata: SubtitleMetadata {
57                title: None,
58                language: None,
59                encoding: "utf-8".to_string(),
60                frame_rate: None,
61                original_format: SubtitleFormatType::Srt,
62            },
63            format: SubtitleFormatType::Srt,
64        })
65    }
66
67    fn serialize(&self, subtitle: &Subtitle) -> Result<String> {
68        let mut output = String::new();
69
70        for (i, entry) in subtitle.entries.iter().enumerate() {
71            output.push_str(&format!("{}\n", i + 1));
72            output.push_str(&format_time_range(entry.start_time, entry.end_time));
73            output.push_str(&format!("{}\n\n", entry.text));
74        }
75
76        Ok(output)
77    }
78
79    fn detect(&self, content: &str) -> bool {
80        let time_pattern =
81            Regex::new(r"\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}").unwrap();
82        time_pattern.is_match(content)
83    }
84
85    fn format_name(&self) -> &'static str {
86        "SRT"
87    }
88
89    fn file_extensions(&self) -> &'static [&'static str] {
90        &["srt"]
91    }
92}
93
94fn parse_time(caps: &regex::Captures, start_group: usize) -> Result<Duration> {
95    let hours: u64 = caps[start_group]
96        .parse()
97        .map_err(|e| SubXError::subtitle_format("SRT", format!("時間值解析失敗: {}", e)))?;
98    let minutes: u64 = caps[start_group + 1]
99        .parse()
100        .map_err(|e| SubXError::subtitle_format("SRT", format!("時間值解析失敗: {}", e)))?;
101    let seconds: u64 = caps[start_group + 2]
102        .parse()
103        .map_err(|e| SubXError::subtitle_format("SRT", format!("時間值解析失敗: {}", e)))?;
104    let milliseconds: u64 = caps[start_group + 3]
105        .parse()
106        .map_err(|e| SubXError::subtitle_format("SRT", format!("時間值解析失敗: {}", e)))?;
107
108    Ok(Duration::from_millis(
109        hours * 3600000 + minutes * 60000 + seconds * 1000 + milliseconds,
110    ))
111}
112
113fn format_time_range(start: Duration, end: Duration) -> String {
114    format!("{} --> {}\n", format_duration(start), format_duration(end))
115}
116
117fn format_duration(duration: Duration) -> String {
118    let total_ms = duration.as_millis();
119    let hours = total_ms / 3600000;
120    let minutes = (total_ms % 3600000) / 60000;
121    let seconds = (total_ms % 60000) / 1000;
122    let milliseconds = total_ms % 1000;
123
124    format!(
125        "{:02}:{:02}:{:02},{:03}",
126        hours, minutes, seconds, milliseconds
127    )
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::core::formats::{SubtitleFormat, SubtitleFormatType};
134    use std::time::Duration;
135
136    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";
137
138    #[test]
139    fn test_srt_parsing_basic() {
140        let format = SrtFormat;
141        let subtitle = format.parse(SAMPLE_SRT).unwrap();
142
143        assert_eq!(subtitle.entries.len(), 2);
144        assert_eq!(subtitle.format, SubtitleFormatType::Srt);
145
146        let first = &subtitle.entries[0];
147        assert_eq!(first.index, 1);
148        assert_eq!(first.start_time, Duration::from_millis(1000));
149        assert_eq!(first.end_time, Duration::from_millis(3000));
150        assert_eq!(first.text, "Hello, World!");
151
152        let second = &subtitle.entries[1];
153        assert_eq!(second.index, 2);
154        assert_eq!(second.start_time, Duration::from_millis(5000));
155        assert_eq!(second.end_time, Duration::from_millis(8000));
156        assert_eq!(second.text, "This is a test subtitle.\n多行測試");
157    }
158
159    #[test]
160    fn test_srt_serialization_roundtrip() {
161        let format = SrtFormat;
162        let subtitle = format.parse(SAMPLE_SRT).unwrap();
163        let serialized = format.serialize(&subtitle).unwrap();
164        let reparsed = format.parse(&serialized).unwrap();
165        assert_eq!(subtitle.entries.len(), reparsed.entries.len());
166        for (o, r) in subtitle.entries.iter().zip(reparsed.entries.iter()) {
167            assert_eq!(o.start_time, r.start_time);
168            assert_eq!(o.end_time, r.end_time);
169            assert_eq!(o.text, r.text);
170        }
171    }
172
173    #[test]
174    fn test_srt_detection() {
175        let format = SrtFormat;
176        assert!(format.detect(SAMPLE_SRT));
177        assert!(!format.detect("This is not SRT content"));
178        assert!(!format.detect("WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nHello"));
179    }
180
181    #[test]
182    fn test_srt_invalid_format() {
183        let format = SrtFormat;
184        let invalid_time = "1\n00:00:01 --> 00:00:03\nText\n\n";
185        let subtitle = format.parse(invalid_time).unwrap();
186        assert_eq!(subtitle.entries.len(), 0);
187        let invalid_index = "invalid\n00:00:01,000 --> 00:00:03,000\nText\n\n";
188        assert!(format.parse(invalid_index).is_err());
189    }
190
191    #[test]
192    fn test_srt_empty_and_malformed_blocks() {
193        let format = SrtFormat;
194        let subtitle = format.parse("").unwrap();
195        assert_eq!(subtitle.entries.len(), 0);
196        let subtitle = format.parse("\n\n\n").unwrap();
197        assert_eq!(subtitle.entries.len(), 0);
198        let malformed = "1\n00:00:01,000 --> 00:00:03,000\n\n";
199        let subtitle = format.parse(malformed).unwrap();
200        assert_eq!(subtitle.entries.len(), 0);
201    }
202
203    #[test]
204    fn test_time_parsing_edge_cases() {
205        let format = SrtFormat;
206        let edge = "1\n23:59:59,999 --> 23:59:59,999\nEnd of day\n\n";
207        let subtitle = format.parse(edge).unwrap();
208        assert_eq!(subtitle.entries.len(), 1);
209        let entry = &subtitle.entries[0];
210        let expected = Duration::from_millis(23 * 3600000 + 59 * 60000 + 59 * 1000 + 999);
211        assert_eq!(entry.start_time, expected);
212        assert_eq!(entry.end_time, expected);
213    }
214
215    #[test]
216    fn test_file_extensions_and_name() {
217        let format = SrtFormat;
218        assert_eq!(format.file_extensions(), &["srt"]);
219        assert_eq!(format.format_name(), "SRT");
220    }
221}