use std::path::Path;
use crate::cli::{ConvertArgs, OutputSubtitleFormat};
use crate::config::ConfigService;
use crate::core::file_manager::FileManager;
use crate::core::formats::converter::{ConversionConfig, FormatConverter};
use crate::error::SubXError;
pub async fn execute(args: ConvertArgs, config_service: &dyn ConfigService) -> crate::Result<()> {
let app_config = config_service.get_config()?;
let config = ConversionConfig {
preserve_styling: app_config.formats.preserve_styling,
target_encoding: args.encoding.clone(),
keep_original: args.keep_original,
validate_output: true,
};
let converter = FormatConverter::new(config);
let default_output = match app_config.formats.default_output.as_str() {
"srt" => OutputSubtitleFormat::Srt,
"ass" => OutputSubtitleFormat::Ass,
"vtt" => OutputSubtitleFormat::Vtt,
"sub" => OutputSubtitleFormat::Sub,
other => {
return Err(SubXError::config(format!(
"Unknown default output format: {other}"
)));
}
};
let output_format = args.format.clone().unwrap_or(default_output);
let handler = args
.get_input_handler()
.map_err(|e| SubXError::CommandExecution(e.to_string()))?;
let collected = handler
.collect_files()
.map_err(|e| SubXError::CommandExecution(e.to_string()))?;
if collected.is_empty() {
return Ok(());
}
for input_path in collected.iter() {
let fmt = output_format.to_string();
let output_path = if let Some(ref o) = args.output {
let mut p = o.clone();
#[allow(clippy::collapsible_if)]
if p.is_dir()
&& (handler.paths.len() != 1 || handler.paths[0].is_dir() || collected.len() > 1)
{
if let Some(stem) = input_path.file_stem().and_then(|s| s.to_str()) {
p.push(format!("{stem}.{fmt}"));
}
}
p
} else if let Some(archive_path) = collected.archive_origin(input_path) {
let archive_dir = archive_path.parent().unwrap_or(Path::new("."));
let stem = input_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
archive_dir.join(format!("{stem}.{fmt}"))
} else {
input_path.with_extension(fmt.clone())
};
match converter.convert_file(input_path, &output_path, &fmt).await {
Ok(result) => {
if result.success {
println!(
"✓ Conversion completed: {} -> {}",
input_path.display(),
output_path.display()
);
if !args.keep_original {
let _ = FileManager::new().remove_file(input_path);
}
} else {
eprintln!("✗ Conversion failed for {}", input_path.display());
for err in result.errors {
eprintln!(" Error: {err}");
}
}
}
Err(e) => {
eprintln!("✗ Conversion error for {}: {}", input_path.display(), e);
}
}
}
Ok(())
}
pub async fn execute_with_config(
args: ConvertArgs,
config_service: std::sync::Arc<dyn ConfigService>,
) -> crate::Result<()> {
execute(args, config_service.as_ref()).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{TestConfigBuilder, TestConfigService};
use std::fs;
use std::sync::Arc;
use tempfile::TempDir;
#[tokio::test]
async fn test_convert_srt_to_vtt() -> crate::Result<()> {
let config_service = Arc::new(TestConfigService::with_defaults());
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("test.srt");
let output_file = temp_dir.path().join("test.vtt");
fs::write(
&input_file,
"1\n00:00:01,000 --> 00:00:02,000\nTest subtitle\n\n",
)
.unwrap();
let args = ConvertArgs {
input: Some(input_file.clone()),
input_paths: Vec::new(),
recursive: false,
format: Some(OutputSubtitleFormat::Vtt),
output: Some(output_file.clone()),
keep_original: false,
encoding: String::from("utf-8"),
no_extract: false,
};
execute_with_config(args, config_service).await?;
let content = fs::read_to_string(&output_file).unwrap();
assert!(content.contains("WEBVTT"));
assert!(content.contains("00:00:01.000 --> 00:00:02.000"));
Ok(())
}
#[tokio::test]
async fn test_convert_batch_processing() -> crate::Result<()> {
let config_service = Arc::new(TestConfigService::with_defaults());
let temp_dir = TempDir::new().unwrap();
for i in 1..=3 {
let file = temp_dir.path().join(format!("test{}.srt", i));
fs::write(
&file,
format!(
"1\n00:00:0{},000 --> 00:00:0{},000\nTest {}\n\n",
i,
i + 1,
i
),
)
.unwrap();
}
let args = ConvertArgs {
input: Some(temp_dir.path().to_path_buf()),
input_paths: Vec::new(),
recursive: false,
format: Some(OutputSubtitleFormat::Vtt),
output: Some(temp_dir.path().join("output")),
keep_original: false,
encoding: String::from("utf-8"),
no_extract: false,
};
execute_with_config(args, config_service).await?;
Ok(())
}
#[tokio::test]
async fn test_convert_unsupported_format() {
let config_service = Arc::new(TestConfigService::with_defaults());
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("test.unknown");
fs::write(&input_file, "not a subtitle").unwrap();
let args = ConvertArgs {
input: Some(input_file),
input_paths: Vec::new(),
recursive: false,
format: Some(OutputSubtitleFormat::Srt),
output: None,
keep_original: false,
encoding: String::from("utf-8"),
no_extract: false,
};
let result = execute_with_config(args, config_service).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_convert_with_different_config() {
let config = TestConfigBuilder::new()
.with_ai_provider("test")
.with_ai_model("test-model")
.build_config();
let config_service = Arc::new(TestConfigService::new(config));
let temp_dir = TempDir::new().unwrap();
let input_file = temp_dir.path().join("test.srt");
let output_file = temp_dir.path().join("test.vtt");
fs::write(
&input_file,
"1\n00:00:01,000 --> 00:00:02,000\nCustom test\n\n",
)
.unwrap();
let args = ConvertArgs {
input: Some(input_file.clone()),
input_paths: Vec::new(),
recursive: false,
format: Some(OutputSubtitleFormat::Vtt),
output: Some(output_file.clone()),
keep_original: true,
encoding: String::from("utf-8"),
no_extract: false,
};
let result = execute_with_config(args, config_service).await;
if result.is_err() {
println!("Test with custom config failed as expected due to external dependencies");
}
}
}