subx_cli/core/formats/
converter.rs1use futures::future::join_all;
16use std::path::Path;
17use std::sync::Arc;
18use tokio::sync::Semaphore;
19
20use crate::Result;
21use crate::core::formats::Subtitle;
22use crate::core::formats::manager::FormatManager;
23
24pub struct FormatConverter {
29 format_manager: FormatManager,
30 pub(crate) config: ConversionConfig,
31}
32
33impl Clone for FormatConverter {
34 fn clone(&self) -> Self {
35 FormatConverter::new(self.config.clone())
36 }
37}
38
39#[derive(Debug, Clone)]
41pub struct ConversionConfig {
42 pub preserve_styling: bool,
44 pub target_encoding: String,
46 pub keep_original: bool,
48 pub validate_output: bool,
50}
51
52#[derive(Debug)]
57pub struct ConversionResult {
58 pub success: bool,
60 pub input_format: String,
62 pub output_format: String,
64 pub original_entries: usize,
66 pub converted_entries: usize,
68 pub warnings: Vec<String>,
70 pub errors: Vec<String>,
72}
73
74impl FormatConverter {
75 pub fn new(config: ConversionConfig) -> Self {
77 Self {
78 format_manager: FormatManager::new(),
79 config,
80 }
81 }
82
83 pub async fn convert_file(
85 &self,
86 input_path: &Path,
87 output_path: &Path,
88 target_format: &str,
89 ) -> crate::Result<ConversionResult> {
90 let input_content = self.read_file_with_encoding(input_path).await?;
92 let input_subtitle = self.format_manager.parse_auto(&input_content)?;
93
94 let converted_subtitle = self.transform_subtitle(input_subtitle.clone(), target_format)?;
96
97 let target_formatter = self
99 .format_manager
100 .get_format(target_format)
101 .ok_or_else(|| {
102 crate::error::SubXError::subtitle_format(
103 format!("Unsupported target format: {}", target_format),
104 "",
105 )
106 })?;
107
108 let output_content = target_formatter.serialize(&converted_subtitle)?;
109
110 self.write_file_with_encoding(output_path, &output_content)
112 .await?;
113
114 let result = if self.config.validate_output {
116 self.validate_conversion(&input_subtitle, &converted_subtitle)
117 .await?
118 } else {
119 ConversionResult {
120 success: true,
121 input_format: input_subtitle.format.to_string(),
122 output_format: target_format.to_string(),
123 original_entries: input_subtitle.entries.len(),
124 converted_entries: converted_subtitle.entries.len(),
125 warnings: Vec::new(),
126 errors: Vec::new(),
127 }
128 };
129 Ok(result)
130 }
131
132 pub async fn convert_batch(
134 &self,
135 input_dir: &Path,
136 target_format: &str,
137 recursive: bool,
138 ) -> crate::Result<Vec<ConversionResult>> {
139 let subtitle_files = self.discover_subtitle_files(input_dir, recursive).await?;
140 let semaphore = Arc::new(Semaphore::new(4));
141
142 let tasks = subtitle_files.into_iter().map(|file_path| {
143 let sem = semaphore.clone();
144 let converter = self.clone();
145 let format = target_format.to_string();
146 async move {
147 let _permit = sem.acquire().await.unwrap();
148 let output_path = file_path.with_extension(&format);
149 converter
150 .convert_file(&file_path, &output_path, &format)
151 .await
152 }
153 });
154
155 let results = join_all(tasks).await;
156 results.into_iter().collect::<Result<Vec<_>>>()
157 }
158 async fn discover_subtitle_files(
160 &self,
161 input_dir: &Path,
162 recursive: bool,
163 ) -> crate::Result<Vec<std::path::PathBuf>> {
164 let discovery = crate::core::matcher::discovery::FileDiscovery::new();
165 let media_files = discovery.scan_directory(input_dir, recursive)?;
166 let paths = media_files
167 .into_iter()
168 .filter(|f| {
169 matches!(
170 f.file_type,
171 crate::core::matcher::discovery::MediaFileType::Subtitle
172 )
173 })
174 .map(|f| f.path) .collect();
176 Ok(paths)
177 }
178
179 async fn read_file_with_encoding(&self, path: &Path) -> crate::Result<String> {
181 let bytes = tokio::fs::read(path).await?;
182 let detector = crate::core::formats::encoding::EncodingDetector::with_defaults();
184 let info = detector.detect_encoding(&bytes)?;
185 let converter = crate::core::formats::encoding::EncodingConverter::new();
186 let conversion = converter.convert_to_utf8(&bytes, &info.charset)?;
187 Ok(conversion.converted_text)
188 }
189
190 async fn write_file_with_encoding(&self, path: &Path, content: &str) -> crate::Result<()> {
192 tokio::fs::write(path, content).await?;
193 Ok(())
194 }
195
196 async fn validate_conversion(
198 &self,
199 original: &Subtitle,
200 converted: &Subtitle,
201 ) -> crate::Result<ConversionResult> {
202 let success = original.entries.len() == converted.entries.len();
203 let errors = if success {
204 Vec::new()
205 } else {
206 vec![format!(
207 "Entry count mismatch: {} -> {}",
208 original.entries.len(),
209 converted.entries.len()
210 )]
211 };
212 Ok(ConversionResult {
213 success,
214 input_format: original.format.to_string(),
215 output_format: converted.format.to_string(),
216 original_entries: original.entries.len(),
217 converted_entries: converted.entries.len(),
218 warnings: Vec::new(),
219 errors,
220 })
221 }
222}