subx_cli/core/formats/
manager.rs1use crate::core::formats::{Subtitle, SubtitleFormat};
16use log::{info, warn};
17
18pub struct FormatManager {
23 formats: Vec<Box<dyn SubtitleFormat>>,
24}
25
26impl Default for FormatManager {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl FormatManager {
33 pub fn new() -> Self {
35 Self {
36 formats: vec![
37 Box::new(crate::core::formats::ass::AssFormat),
38 Box::new(crate::core::formats::vtt::VttFormat),
39 Box::new(crate::core::formats::srt::SrtFormat),
40 Box::new(crate::core::formats::sub::SubFormat),
41 ],
42 }
43 }
44
45 pub fn parse_auto(&self, content: &str) -> crate::Result<Subtitle> {
47 for fmt in &self.formats {
48 if fmt.detect(content) {
49 return fmt.parse(content);
50 }
51 }
52 Err(crate::error::SubXError::subtitle_format(
53 "Unknown",
54 "Unknown subtitle format",
55 ))
56 }
57
58 pub fn get_format(&self, name: &str) -> Option<&dyn SubtitleFormat> {
60 let lname = name.to_lowercase();
61 self.formats
62 .iter()
63 .find(|f| f.format_name().to_lowercase() == lname)
64 .map(|f| f.as_ref())
65 }
66
67 pub fn get_format_by_extension(&self, ext: &str) -> Option<&dyn SubtitleFormat> {
69 let ext_lc = ext.to_lowercase();
70 self.formats
71 .iter()
72 .find(|f| f.file_extensions().contains(&ext_lc.as_str()))
73 .map(|f| f.as_ref())
74 }
75
76 pub fn read_subtitle_with_encoding_detection(&self, file_path: &str) -> crate::Result<String> {
78 crate::core::fs_util::check_file_size(
79 std::path::Path::new(file_path),
80 52_428_800, "Subtitle",
82 )?;
83 let detector = crate::core::formats::encoding::EncodingDetector::with_defaults();
84 let info = detector.detect_file_encoding(file_path)?;
85 let converter = crate::core::formats::encoding::EncodingConverter::new();
86 let result = converter.convert_file_to_utf8(file_path, &info)?;
87 let validation = converter.validate_conversion(&result);
88 if !validation.is_valid {
89 warn!("Encoding conversion warnings: {:?}", validation.warnings);
90 }
91 info!(
92 "Detected encoding: {:?} (confidence: {:.2})",
93 info.charset, info.confidence
94 );
95 Ok(result.converted_text)
96 }
97
98 pub fn get_encoding_info(
100 &self,
101 file_path: &str,
102 ) -> crate::Result<crate::core::formats::encoding::EncodingInfo> {
103 let detector = crate::core::formats::encoding::EncodingDetector::with_defaults();
104 detector.detect_file_encoding(file_path)
105 }
106
107 pub fn load_subtitle(&self, file_path: &std::path::Path) -> crate::Result<Subtitle> {
109 crate::core::fs_util::check_file_size(
110 file_path, 52_428_800, "Subtitle",
112 )?;
113 let content =
114 self.read_subtitle_with_encoding_detection(file_path.to_str().ok_or_else(|| {
115 crate::error::SubXError::subtitle_format("", "Invalid file path encoding")
116 })?)?;
117 self.parse_auto(&content)
118 }
119
120 pub fn save_subtitle(
122 &self,
123 subtitle: &Subtitle,
124 file_path: &std::path::Path,
125 ) -> crate::Result<()> {
126 let ext = file_path.extension().and_then(|s| s.to_str()).unwrap_or("");
127 let fmt = self.get_format_by_extension(ext).ok_or_else(|| {
128 crate::error::SubXError::subtitle_format(ext, "Unsupported subtitle format for saving")
129 })?;
130 let out = fmt.serialize(subtitle)?;
131 std::fs::write(file_path, out)?;
132 Ok(())
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::core::formats::SubtitleFormatType;
140 use std::time::Duration;
141
142 const SAMPLE_SRT: &str = "1\n00:00:00,000 --> 00:00:01,000\nOne\n";
143 const SAMPLE_VTT: &str = "WEBVTT\n\n1\n00:00:00.000 --> 00:00:01.000\nOne\n";
144 const SAMPLE_WEBVTT_THREE_LINES: &str = "WEBVTT\n\n1\n00:00:01.000 --> 00:00:03.000\n第一句字幕內容\n\n2\n00:00:04.000 --> 00:00:06.000\n第二句字幕內容\n\n3\n00:00:07.000 --> 00:00:09.000\n第三句字幕內容\n";
146 const COMPLEX_WEBVTT: &str = "WEBVTT\n\nNOTE 這是註解,應該被忽略\n\nSTYLE\n::cue {\n background-color: black;\n color: white;\n}\n\n1\n00:00:01.000 --> 00:00:03.500\n第一句字幕內容\n包含多行文字\n\n2\n00:00:04.200 --> 00:00:07.800\n第二句字幕內容\n\n3\n00:00:08.000 --> 00:00:10.000\n第三句字幕內容\n";
147
148 #[test]
149 fn test_get_format_by_name_and_extension() {
150 let mgr = FormatManager::new();
151 let srt = mgr.get_format("srt").expect("get_format srt");
152 assert_eq!(srt.format_name(), "SRT");
153 let vtt = mgr
154 .get_format_by_extension("vtt")
155 .expect("get_format_by_extension vtt");
156 assert_eq!(vtt.format_name(), "VTT");
157 }
158
159 #[test]
160 fn test_load_subtitle_rejects_oversized_file() {
161 use std::io::Write;
162 let temp = tempfile::TempDir::new().expect("tempdir");
163 let path = temp.path().join("huge.srt");
164 let mut f = std::fs::File::create(&path).expect("create");
166 let chunk = vec![b'a'; 1024 * 1024];
167 for _ in 0..51 {
168 f.write_all(&chunk).expect("write");
169 }
170 drop(f);
171 let mgr = FormatManager::new();
172 let err = mgr.load_subtitle(&path).unwrap_err();
173 let msg = format!("{}", err);
174 assert!(
175 msg.contains("Subtitle file too large"),
176 "expected oversize error, got: {}",
177 msg
178 );
179 }
180
181 #[test]
182 fn test_parse_auto_supported_and_error() {
183 let mgr = FormatManager::new();
184 let sub = mgr.parse_auto(SAMPLE_SRT).expect("parse_auto srt");
185 assert_eq!(sub.format, SubtitleFormatType::Srt);
186 let subv = mgr.parse_auto(SAMPLE_VTT).expect("parse_auto vtt");
187 assert_eq!(subv.format, SubtitleFormatType::Vtt);
188 let err = mgr.parse_auto("no format");
189 assert!(err.is_err());
190 }
191
192 #[test]
193 fn test_webvtt_parse_auto_first_subtitle_content() {
194 let mgr = FormatManager::new();
195
196 let subtitle = mgr
197 .parse_auto(SAMPLE_WEBVTT_THREE_LINES)
198 .expect("Failed to parse WEBVTT format using parse_auto");
199
200 assert_eq!(
202 subtitle.format,
203 SubtitleFormatType::Vtt,
204 "Auto detection should identify as WEBVTT format"
205 );
206
207 assert_eq!(
209 subtitle.entries.len(),
210 3,
211 "Should parse exactly 3 subtitle entries"
212 );
213
214 let first = &subtitle.entries[0];
216 assert_eq!(
217 first.text, "第一句字幕內容",
218 "First subtitle content should be correctly parsed"
219 );
220 assert_eq!(first.index, 1, "First subtitle should have index 1");
221 assert_eq!(
222 first.start_time,
223 Duration::from_millis(1000),
224 "First subtitle start time should be 1 second"
225 );
226 assert_eq!(
227 first.end_time,
228 Duration::from_millis(3000),
229 "First subtitle end time should be 3 seconds"
230 );
231
232 assert_eq!(subtitle.entries[1].text, "第二句字幕內容");
234 assert_eq!(subtitle.entries[2].text, "第三句字幕內容");
235 }
236
237 #[test]
238 fn test_webvtt_parse_auto_with_complex_content() {
239 let mgr = FormatManager::new();
240 let subtitle = mgr
241 .parse_auto(COMPLEX_WEBVTT)
242 .expect("Failed to parse complex WEBVTT");
243
244 assert_eq!(subtitle.format, SubtitleFormatType::Vtt);
246 assert_eq!(subtitle.entries.len(), 3);
247
248 let first = &subtitle.entries[0];
250 assert_eq!(first.text, "第一句字幕內容\n包含多行文字");
251 assert_eq!(first.start_time, Duration::from_millis(1000));
252 assert_eq!(first.end_time, Duration::from_millis(3500));
253 }
254}