use futures::future::join_all;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::Semaphore;
use crate::Result;
use crate::core::formats::Subtitle;
use crate::core::formats::manager::FormatManager;
pub struct FormatConverter {
format_manager: FormatManager,
pub(crate) config: ConversionConfig,
}
impl Clone for FormatConverter {
fn clone(&self) -> Self {
FormatConverter::new(self.config.clone())
}
}
#[derive(Debug, Clone)]
pub struct ConversionConfig {
pub preserve_styling: bool,
pub target_encoding: String,
pub keep_original: bool,
pub validate_output: bool,
}
#[derive(Debug)]
pub struct ConversionResult {
pub success: bool,
pub input_format: String,
pub output_format: String,
pub original_entries: usize,
pub converted_entries: usize,
pub warnings: Vec<String>,
pub errors: Vec<String>,
}
impl FormatConverter {
pub fn new(config: ConversionConfig) -> Self {
Self {
format_manager: FormatManager::new(),
config,
}
}
pub async fn convert_file(
&self,
input_path: &Path,
output_path: &Path,
target_format: &str,
) -> crate::Result<ConversionResult> {
let input_content = self.read_file_with_encoding(input_path).await?;
let input_subtitle = self.format_manager.parse_auto(&input_content)?;
let converted_subtitle = self.transform_subtitle(input_subtitle.clone(), target_format)?;
let target_formatter = self
.format_manager
.get_format(target_format)
.ok_or_else(|| {
crate::error::SubXError::subtitle_format(
format!("Unsupported target format: {}", target_format),
"",
)
})?;
let output_content = target_formatter.serialize(&converted_subtitle)?;
self.write_file_with_encoding(output_path, &output_content)
.await?;
let result = if self.config.validate_output {
self.validate_conversion(&input_subtitle, &converted_subtitle)
.await?
} else {
ConversionResult {
success: true,
input_format: input_subtitle.format.to_string(),
output_format: target_format.to_string(),
original_entries: input_subtitle.entries.len(),
converted_entries: converted_subtitle.entries.len(),
warnings: Vec::new(),
errors: Vec::new(),
}
};
Ok(result)
}
pub async fn convert_batch(
&self,
input_dir: &Path,
target_format: &str,
recursive: bool,
) -> crate::Result<Vec<ConversionResult>> {
let subtitle_files = self.discover_subtitle_files(input_dir, recursive).await?;
let semaphore = Arc::new(Semaphore::new(4));
let tasks = subtitle_files.into_iter().map(|file_path| {
let sem = semaphore.clone();
let converter = self.clone();
let format = target_format.to_string();
async move {
let _permit = sem.acquire().await.unwrap();
let output_path = file_path.with_extension(&format);
converter
.convert_file(&file_path, &output_path, &format)
.await
}
});
let results = join_all(tasks).await;
results.into_iter().collect::<Result<Vec<_>>>()
}
async fn discover_subtitle_files(
&self,
input_dir: &Path,
recursive: bool,
) -> crate::Result<Vec<std::path::PathBuf>> {
let discovery = crate::core::matcher::discovery::FileDiscovery::new();
let media_files = discovery.scan_directory(input_dir, recursive)?;
let paths = media_files
.into_iter()
.filter(|f| {
matches!(
f.file_type,
crate::core::matcher::discovery::MediaFileType::Subtitle
)
})
.map(|f| f.path) .collect();
Ok(paths)
}
async fn read_file_with_encoding(&self, path: &Path) -> crate::Result<String> {
crate::core::fs_util::check_file_size(path, 52_428_800, "Subtitle")?;
let bytes = tokio::fs::read(path).await?;
let detector = crate::core::formats::encoding::EncodingDetector::with_defaults();
let info = detector.detect_encoding(&bytes)?;
let converter = crate::core::formats::encoding::EncodingConverter::new();
let conversion = converter.convert_to_utf8(&bytes, &info.charset)?;
Ok(conversion.converted_text)
}
async fn write_file_with_encoding(&self, path: &Path, content: &str) -> crate::Result<()> {
tokio::fs::write(path, content).await?;
Ok(())
}
async fn validate_conversion(
&self,
original: &Subtitle,
converted: &Subtitle,
) -> crate::Result<ConversionResult> {
let success = original.entries.len() == converted.entries.len();
let errors = if success {
Vec::new()
} else {
vec![format!(
"Entry count mismatch: {} -> {}",
original.entries.len(),
converted.entries.len()
)]
};
Ok(ConversionResult {
success,
input_format: original.format.to_string(),
output_format: converted.format.to_string(),
original_entries: original.entries.len(),
converted_entries: converted.entries.len(),
warnings: Vec::new(),
errors,
})
}
}