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