1use crate::cli::SyncArgs;
8use crate::cli::SyncMode;
9use crate::config::Config;
10use crate::config::ConfigService;
11use crate::core::formats::manager::FormatManager;
12use crate::core::sync::{SyncEngine, SyncMethod, SyncResult};
13use crate::{Result, error::SubXError};
14
15async fn run_single(
17 args: &SyncArgs,
18 config: &Config,
19 sync_engine: &SyncEngine,
20 format_manager: &FormatManager,
21) -> Result<()> {
22 let subtitle_path = args.subtitle.as_ref().ok_or_else(|| {
23 SubXError::CommandExecution(
24 "Subtitle file path is required for single file sync".to_string(),
25 )
26 })?;
27
28 if args.verbose {
29 println!("🎬 Loading subtitle file: {}", subtitle_path.display());
30 println!("📄 Subtitle entries count: {}", {
31 let s = format_manager.load_subtitle(subtitle_path)?;
32 s.entries.len()
33 });
34 }
35 let mut subtitle = format_manager.load_subtitle(subtitle_path)?;
36 let sync_result = if let Some(offset) = args.offset {
37 if args.verbose {
38 println!("⚙️ Using manual offset: {:.3}s", offset);
39 }
40 sync_engine.apply_manual_offset(&mut subtitle, offset)?;
41 SyncResult {
42 offset_seconds: offset,
43 confidence: 1.0,
44 method_used: crate::core::sync::SyncMethod::Manual,
45 correlation_peak: 0.0,
46 processing_duration: std::time::Duration::ZERO,
47 warnings: Vec::new(),
48 additional_info: None,
49 }
50 } else {
51 let video_path = args.video.as_ref().ok_or_else(|| {
53 SubXError::CommandExecution(
54 "Video file path is required for automatic sync".to_string(),
55 )
56 })?;
57 let method = determine_sync_method(args, &config.sync.default_method)?;
58 if args.verbose {
59 println!("🔍 Starting sync analysis...");
60 println!(" Method: {:?}", method);
61 println!(" Analysis window: {}s", args.window);
62 println!(" Video file: {}", video_path.display());
63 }
64 let mut sync_cfg = config.sync.clone();
65 apply_cli_overrides(&mut sync_cfg, args)?;
66 let result = sync_engine
67 .detect_sync_offset(video_path.as_path(), &subtitle, Some(method))
68 .await?;
69 if args.verbose {
70 println!("✅ Analysis completed:");
71 println!(" Detected offset: {:.3}s", result.offset_seconds);
72 println!(" Confidence: {:.1}%", result.confidence * 100.0);
73 println!(" Processing time: {:?}", result.processing_duration);
74 }
75 if !args.dry_run {
76 sync_engine.apply_manual_offset(&mut subtitle, result.offset_seconds)?;
77 }
78 result
79 };
80 display_sync_result(&sync_result, args.verbose);
81 if !args.dry_run {
82 if let Some(out) = args.get_output_path() {
83 if out.exists() && !args.force {
84 return Err(SubXError::CommandExecution(format!(
85 "Output file already exists: {}. Use --force to overwrite.",
86 out.display()
87 )));
88 }
89 format_manager.save_subtitle(&subtitle, &out)?;
90 if args.verbose {
91 println!("💾 Synchronized subtitle saved to: {}", out.display());
92 } else {
93 println!("Synchronized subtitle saved to: {}", out.display());
94 }
95 } else {
96 return Err(SubXError::CommandExecution(
97 "No output path specified".to_string(),
98 ));
99 }
100 } else {
101 println!("🔍 Dry run mode - file not saved");
102 }
103 Ok(())
104}
105
106pub async fn execute(args: SyncArgs, config_service: &dyn ConfigService) -> Result<()> {
133 if let Err(msg) = args.validate() {
135 return Err(SubXError::CommandExecution(msg));
136 }
137 let config = config_service.get_config()?;
138
139 if let Some(manual_offset) = args.offset {
141 if manual_offset.abs() > config.sync.max_offset_seconds {
142 return Err(SubXError::config(format!(
143 "The specified offset {:.2}s exceeds the configured maximum allowed value {:.2}s.\n\n\
144 Please use one of the following methods to resolve this issue:\n\
145 1. Use a smaller offset: --offset {:.2}\n\
146 2. Adjust configuration: subx-cli config set sync.max_offset_seconds {:.2}\n\
147 3. Use automatic detection: remove the --offset parameter",
148 manual_offset,
149 config.sync.max_offset_seconds,
150 config.sync.max_offset_seconds * 0.9, manual_offset
152 .abs()
153 .max(config.sync.max_offset_seconds * 1.5) )));
155 }
156 }
157
158 let sync_engine = SyncEngine::new(config.sync.clone())?;
159 let format_manager = FormatManager::new();
160
161 if let Ok(SyncMode::Batch(handler)) = args.get_sync_mode() {
163 let paths = handler
164 .collect_files()
165 .map_err(|e| SubXError::CommandExecution(e.to_string()))?;
166 for path in &paths {
167 if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
168 let ext = ext.to_lowercase();
169 if ["mp4", "mkv", "avi", "mov"].contains(&ext.as_str()) {
170 let stem = path
171 .file_stem()
172 .map(|s| s.to_string_lossy().to_string())
173 .unwrap_or_default();
174 if let Some(sub_path) = paths.iter().find(|p| {
175 p.file_stem()
176 .map(|s| s.to_string_lossy() == stem)
177 .unwrap_or(false)
178 && p.extension()
179 .and_then(|s| s.to_str())
180 .map(|e| {
181 matches!(
182 e.to_lowercase().as_str(),
183 "srt" | "ass" | "vtt" | "sub"
184 )
185 })
186 .unwrap_or(false)
187 }) {
188 let mut single_args = args.clone();
189 single_args.input_paths.clear();
190 single_args.batch = false;
191 single_args.recursive = false;
192 single_args.video = Some(path.clone());
193 single_args.subtitle = Some(sub_path.clone());
194 run_single(&single_args, &config, &sync_engine, &format_manager).await?;
195 } else {
196 eprintln!("✗ Skip sync for {}: no matching subtitle", path.display());
197 }
198 }
199 }
200 }
201 return Ok(());
202 }
203
204 match args.get_sync_mode() {
206 Ok(SyncMode::Single { .. }) => {
207 run_single(&args, &config, &sync_engine, &format_manager).await?;
208 Ok(())
209 }
210 Err(err) => Err(err),
211 _ => unreachable!(),
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::config::TestConfigService;
219 use std::fs;
220 use std::sync::Arc;
221 use tempfile::TempDir;
222
223 #[tokio::test]
224 async fn test_sync_batch_processing() -> Result<()> {
225 let config_service = Arc::new(TestConfigService::with_sync_settings(0.5, 30.0));
227
228 let tmp = TempDir::new().unwrap();
230 let video1 = tmp.path().join("movie1.mp4");
231 let sub1 = tmp.path().join("movie1.srt");
232 fs::write(&video1, b"").unwrap();
233 fs::write(&sub1, b"1\n00:00:01,000 --> 00:00:02,000\nTest1\n\n").unwrap();
234
235 let args = SyncArgs {
237 video: Some(video1.clone()),
238 subtitle: Some(sub1.clone()),
239 input_paths: vec![],
240 recursive: false,
241 offset: Some(1.0), method: Some(crate::cli::SyncMethodArg::Manual),
243 window: 30,
244 vad_sensitivity: None,
245 output: None,
246 verbose: false,
247 dry_run: true, force: true,
249 batch: false, #[allow(deprecated)]
251 range: None,
252 #[allow(deprecated)]
253 threshold: None,
254 };
255
256 execute(args, config_service.as_ref()).await?;
257
258 Ok(())
260 }
261}
262
263pub async fn execute_with_config(
265 args: SyncArgs,
266 config_service: std::sync::Arc<dyn ConfigService>,
267) -> Result<()> {
268 execute(args, config_service.as_ref()).await
269}
270
271fn determine_sync_method(args: &SyncArgs, default_method: &str) -> Result<SyncMethod> {
282 if let Some(ref method_arg) = args.method {
284 return Ok(method_arg.clone().into());
285 }
286
287 match default_method {
289 "vad" => Ok(SyncMethod::LocalVad),
290 "auto" => Ok(SyncMethod::Auto),
291 _ => Ok(SyncMethod::Auto),
292 }
293}
294
295fn apply_cli_overrides(config: &mut crate::config::SyncConfig, args: &SyncArgs) -> Result<()> {
302 if let Some(sensitivity) = args.vad_sensitivity {
304 config.vad.sensitivity = sensitivity;
305 }
306
307 Ok(())
308}
309
310fn display_sync_result(result: &SyncResult, verbose: bool) {
317 if verbose {
318 println!("\n=== Sync Results ===");
319 println!("Method used: {:?}", result.method_used);
320 println!("Detected offset: {:.3} seconds", result.offset_seconds);
321 println!("Confidence: {:.1}%", result.confidence * 100.0);
322 println!("Processing time: {:?}", result.processing_duration);
323
324 if !result.warnings.is_empty() {
325 println!("\nWarnings:");
326 for warning in &result.warnings {
327 println!(" ⚠️ {}", warning);
328 }
329 }
330
331 if let Some(info) = &result.additional_info {
332 if let Ok(pretty_info) = serde_json::to_string_pretty(info) {
333 println!("\nAdditional information:");
334 println!("{}", pretty_info);
335 }
336 }
337 } else {
338 println!(
339 "✅ Sync completed: offset {:.3}s (confidence: {:.1}%)",
340 result.offset_seconds,
341 result.confidence * 100.0
342 );
343 }
344}