subx_cli/lib.rs
1//! SubX: Intelligent Subtitle Processing Library
2//!
3//! SubX is a comprehensive Rust library for intelligent subtitle file processing,
4//! featuring AI-powered matching, format conversion, audio synchronization,
5//! and advanced encoding detection capabilities.
6//!
7//! # Key Features
8//!
9//! - **AI-Powered Matching**: Intelligent subtitle file matching and renaming
10//! - **Format Conversion**: Support for multiple subtitle formats (SRT, ASS, VTT, etc.)
11//! - **Audio Synchronization**: Advanced audio-subtitle timing adjustment
12//! - **Encoding Detection**: Automatic character encoding detection and conversion
13//! - **Parallel Processing**: High-performance batch operations
14//! - **Configuration Management**: Flexible multi-source configuration system
15//!
16//! # Architecture Overview
17//!
18//! The library is organized into several key modules:
19//!
20//! - [`cli`] - Command-line interface and argument parsing
21//! - [`commands`] - Implementation of all SubX commands
22//!
23//! The [`config`], [`core`], [`error`] and [`services`] modules are owned by
24//! the `subx_core` crate — the library half of the SubX two-crate split, a
25//! git submodule mounted at `subx-core/` and a member of this Cargo
26//! workspace. They are re-exported below under their pre-split paths so
27//! existing consumers keep compiling; `subx_core::` is the canonical path.
28//!
29//! # Quick Start
30//!
31//! ```rust,no_run
32//! use subx_cli::config::{TestConfigService, ConfigService};
33//!
34//! // Create a configuration service
35//! let config_service = TestConfigService::with_defaults();
36//! let config = config_service.config();
37//!
38//! // Use the configuration for processing...
39//! ```
40//!
41//! # Error Handling
42//!
43//! All operations return a [`Result<T>`] type that wraps [`error::SubXError`]:
44//!
45//! ```rust
46//! use subx_cli::{Result, error::SubXError};
47//!
48//! fn example_operation() -> Result<String> {
49//! // This could fail with various error types
50//! Err(SubXError::config("Missing configuration"))
51//! }
52//! ```
53//!
54//! # Configuration
55//!
56//! SubX supports dependency injection-based configuration:
57//!
58//! ```rust,no_run
59//! use subx_cli::config::{TestConfigService, Config};
60//!
61//! // Create configuration service with AI settings
62//! let config_service = TestConfigService::with_ai_settings("openai", "gpt-4.1");
63//! let config = config_service.config();
64//!
65//! // Access configuration values
66//! println!("AI Provider: {}", config.ai.provider);
67//! println!("AI Model: {}", config.ai.model);
68//! ```
69//!
70//! # Performance Considerations
71//!
72//! - Use [`core::parallel`] for batch operations on large file sets
73//! - Configure appropriate cache settings for repeated operations
74//! - Consider memory usage when processing large audio files
75//!
76//! # Thread Safety
77//!
78//! The library is designed to be thread-safe where appropriate:
79//! - Configuration manager uses `Arc<RwLock<T>>` for shared state
80//! - File operations include rollback capabilities for atomicity
81//! - Parallel processing uses safe concurrency patterns
82//!
83//! # Feature Flags
84//!
85//! Both optional features of this crate are pass-throughs that enable the
86//! matching gate in the `subx-core` crate (where the gated sources live) and
87//! nothing else:
88//! ```text
89//! - archive-rar - enable RAR archive extraction (optional unrar dependency)
90//! - slow-tests - compile long-running tests in both crates
91//! ```
92
93#![allow(
94 clippy::new_without_default,
95 clippy::manual_clamp,
96 clippy::useless_vec,
97 clippy::items_after_test_module,
98 clippy::needless_borrow,
99 clippy::uninlined_format_args,
100 clippy::collapsible_if
101)]
102#![warn(missing_docs)]
103#![warn(rustdoc::missing_crate_level_docs)]
104
105/// Library version string.
106///
107/// This constant provides the current version of the SubX library,
108/// automatically populated from `Cargo.toml` at compile time.
109///
110/// # Examples
111///
112/// ```rust
113/// use subx_cli::VERSION;
114///
115/// println!("SubX version: {}", VERSION);
116/// ```
117pub const VERSION: &str = env!("CARGO_PKG_VERSION");
118
119/// Version of the `subx-core` library this build is linked against.
120///
121/// This constant reports the version of the [`subx_core`] crate resolved at
122/// build time (the git submodule mounted at `subx-core/`), and is distinct
123/// from [`VERSION`], which reports this CLI crate's own version. The two
124/// crates carry independent version lines related only by the caret
125/// requirement in `Cargo.toml`.
126///
127/// # Examples
128///
129/// ```rust
130/// use subx_cli::CORE_VERSION;
131///
132/// assert!(!CORE_VERSION.is_empty());
133/// println!("Linked subx-core version: {}", CORE_VERSION);
134/// ```
135pub const CORE_VERSION: &str = subx_core::VERSION;
136
137pub mod cli;
138pub mod commands;
139
140// ── Legacy back-compat re-exports of the subx-core crate ──────────────────
141//
142// `config`, `core`, `error` and `services` moved to the `subx-core` crate
143// (the library half of the SubX two-crate split). Their canonical paths are
144// now `subx_core::{config, core, error, services}`; the re-exports below
145// exist only so consumers written against the pre-split `subx_cli::` paths
146// keep compiling, and they will be deleted once every out-of-tree consumer
147// (the Tauri GUI first) has migrated. Code inside this crate names
148// `subx_core::…` directly and never resolves through them.
149//
150// Documentation: `docs.rs/subx-core` is the canonical rustdoc for these
151// four modules and everything under them. The re-exported pages that
152// appear on `docs.rs/subx-cli` are generated by the `--workspace` doc
153// build and merely forward there; readers of the library API should go to
154// the `subx-core` documentation directly.
155
156/// Configuration management and validation.
157///
158/// Legacy re-export: the module now lives in the `subx-core` crate; the
159/// canonical path is [`subx_core::config`]. This alias exists for consumers
160/// that have not yet migrated and will be removed once they have.
161pub use subx_core::config;
162/// Core processing engines (formats, matching, sync).
163///
164/// Legacy re-export: the module now lives in the `subx-core` crate; the
165/// canonical path is [`subx_core::core`]. This alias exists for consumers
166/// that have not yet migrated and will be removed once they have.
167pub use subx_core::core;
168/// Comprehensive error handling system.
169///
170/// Legacy re-export: the module now lives in the `subx-core` crate; the
171/// canonical path is [`subx_core::error`]. This alias exists for consumers
172/// that have not yet migrated and will be removed once they have.
173pub use subx_core::error;
174/// External service integrations (AI, audio processing).
175///
176/// Legacy re-export: the module now lives in the `subx-core` crate; the
177/// canonical path is [`subx_core::services`]. This alias exists for
178/// consumers that have not yet migrated and will be removed once they have.
179pub use subx_core::services;
180
181/// Root configuration types.
182///
183/// Legacy re-exports: these types now live in the `subx-core` crate, where
184/// the canonical paths are `subx_core::Config` and `subx_core::ConfigService`
185/// and so on. They exist for consumers that have not yet migrated and will
186/// be removed once they have.
187pub use subx_core::Config;
188pub use subx_core::{
189 ConfigService, EnvironmentProvider, ProductionConfigService, SystemEnvironmentProvider,
190 TestConfigBuilder, TestConfigService, TestEnvironmentProvider,
191};
192
193/// All twelve `#[macro_export]` configuration test macros, re-exported from
194/// the `subx-core` crate.
195///
196/// `#[macro_export]` places a macro at its defining crate's root, so
197/// `pub use subx_core::config;` above cannot reach them; they are listed
198/// individually. Legacy re-exports for consumers that have not yet migrated
199/// to `subx_core::` paths, and will be removed once they have.
200pub use subx_core::{
201 create_default_test_config_service, create_production_config_service_with_empty_env,
202 create_production_config_service_with_env, create_test_config_service,
203 test_production_config_with_env, test_production_config_with_openai_env, test_with_ai_config,
204 test_with_ai_config_and_key, test_with_config, test_with_default_config,
205 test_with_parallel_config, test_with_sync_config,
206};
207
208/// Convenient type alias for `Result<T, SubXError>`.
209///
210/// Denotes the same type as before the crate split: the error type now lives
211/// in the `subx-core` crate, and this alias is the legacy `subx_cli::Result`
212/// path kept for consumers that have not yet migrated to
213/// [`subx_core::Result`].
214pub type Result<T> = subx_core::error::SubXResult<T>;
215
216/// Main application structure with dependency injection support.
217///
218/// The `App` struct provides a programmatic interface to SubX functionality,
219/// designed for embedding SubX in other Rust applications or for advanced
220/// use cases requiring fine-grained control over configuration and execution.
221///
222/// # Use Cases
223///
224/// - **Embedding**: Use SubX as a library component in larger applications
225/// - **Testing**: Programmatic testing of SubX functionality with custom configurations
226/// - **Automation**: Scripted execution of SubX operations without shell commands
227/// - **Custom Workflows**: Building complex workflows that combine multiple SubX operations
228///
229/// # vs CLI Interface
230///
231/// | Feature | CLI (`subx` command) | App (Library API) |
232/// |---------|---------------------|-------------------|
233/// | Usage | Command line tool | Embedded in Rust code |
234/// | Config | Files + Environment | Programmatic injection |
235/// | Output | Terminal/stdout | Programmatic control |
236/// | Error Handling | Exit codes | Result types |
237///
238/// # Examples
239///
240/// ## Basic Usage
241///
242/// ```rust,no_run
243/// use subx_cli::{App, config::ProductionConfigService};
244/// use std::sync::Arc;
245///
246/// # async fn example() -> subx_cli::Result<()> {
247/// let config_service = Arc::new(ProductionConfigService::new()?);
248/// let app = App::new(config_service);
249///
250/// // Execute operations programmatically
251/// app.match_files("/movies", true).await?; // dry run
252/// app.convert_files("/subs", "srt", Some("/output")).await?;
253/// # Ok(())
254/// # }
255/// ```
256///
257/// ## With Custom Configuration
258///
259/// ```rust,no_run
260/// use subx_cli::{App, config::{TestConfigService, Config}};
261/// use std::sync::Arc;
262///
263/// # async fn example() -> subx_cli::Result<()> {
264/// let mut config_service = TestConfigService::with_ai_settings("openai", "gpt-4");
265///
266/// let app = App::new(Arc::new(config_service));
267/// app.match_files("/path", false).await?;
268/// # Ok(())
269/// # }
270/// ```
271pub struct App {
272 config_service: std::sync::Arc<dyn config::ConfigService>,
273}
274
275impl App {
276 /// Create a new application instance with the provided configuration service.
277 ///
278 /// # Arguments
279 ///
280 /// * `config_service` - The configuration service to use
281 ///
282 /// # Examples
283 ///
284 /// ```rust,no_run
285 /// use subx_cli::{App, config::TestConfigService};
286 /// use std::sync::Arc;
287 ///
288 /// let config_service = Arc::new(TestConfigService::with_defaults());
289 /// let app = App::new(config_service);
290 /// ```
291 pub fn new(config_service: std::sync::Arc<dyn config::ConfigService>) -> Self {
292 Self { config_service }
293 }
294
295 /// Create a new application instance with the production configuration service.
296 ///
297 /// This is the default way to create an application instance for production use.
298 ///
299 /// # Examples
300 ///
301 /// ```rust,no_run
302 /// use subx_cli::App;
303 ///
304 /// # async fn example() -> subx_cli::Result<()> {
305 /// let app = App::new_with_production_config()?;
306 /// // Ready to use with production configuration
307 /// # Ok(())
308 /// # }
309 /// ```
310 ///
311 /// # Errors
312 ///
313 /// Returns an error if the production configuration service cannot be created.
314 pub fn new_with_production_config() -> Result<Self> {
315 let config_service = std::sync::Arc::new(config::ProductionConfigService::new()?);
316 Ok(Self::new(config_service))
317 }
318
319 /// Run the application with command-line argument parsing.
320 ///
321 /// This method provides a programmatic way to run SubX with CLI-style
322 /// arguments, useful for embedding SubX in other Rust applications.
323 ///
324 /// # Examples
325 ///
326 /// ```rust,no_run
327 /// use subx_cli::{App, config::ProductionConfigService};
328 /// use std::sync::Arc;
329 ///
330 /// # async fn example() -> subx_cli::Result<()> {
331 /// let config_service = Arc::new(ProductionConfigService::new()?);
332 /// let app = App::new(config_service);
333 ///
334 /// // This parses std::env::args() just like the CLI
335 /// app.run().await?;
336 /// # Ok(())
337 /// # }
338 /// ```
339 ///
340 /// # Errors
341 ///
342 /// Returns an error if command execution fails.
343 pub async fn run(&self) -> Result<()> {
344 let cli = <cli::Cli as clap::Parser>::parse();
345 self.handle_command(cli.command).await
346 }
347
348 /// Handle a specific command with the current configuration.
349 ///
350 /// This method allows programmatic execution of specific SubX commands
351 /// without parsing command-line arguments.
352 ///
353 /// # Examples
354 ///
355 /// ```rust,no_run
356 /// use subx_cli::{App, cli::{Commands, MatchArgs}, config::TestConfigService};
357 /// use std::sync::Arc;
358 ///
359 /// # async fn example() -> subx_cli::Result<()> {
360 /// let config_service = Arc::new(TestConfigService::with_defaults());
361 /// let app = App::new(config_service);
362 ///
363 /// let match_args = MatchArgs {
364 /// path: Some("/path/to/files".into()),
365 /// input_paths: vec![],
366 /// dry_run: true,
367 /// confidence: 80,
368 /// recursive: false,
369 /// backup: false,
370 /// copy: false,
371 /// move_files: false,
372 /// no_extract: false,
373 /// };
374 ///
375 /// app.handle_command(Commands::Match(match_args)).await?;
376 /// # Ok(())
377 /// # }
378 /// ```
379 ///
380 /// # Arguments
381 ///
382 /// * `command` - The command to execute
383 ///
384 /// # Errors
385 ///
386 /// Returns an error if command execution fails.
387 pub async fn handle_command(&self, command: cli::Commands) -> Result<()> {
388 // Use the centralized dispatcher to eliminate code duplication
389 crate::commands::dispatcher::dispatch_command(command, self.config_service.clone()).await
390 }
391
392 /// Execute a match operation programmatically.
393 ///
394 /// This is a convenience method for programmatic usage without
395 /// needing to construct the Commands enum manually.
396 ///
397 /// # Examples
398 ///
399 /// ```rust,no_run
400 /// use subx_cli::{App, config::TestConfigService};
401 /// use std::sync::Arc;
402 ///
403 /// # async fn example() -> subx_cli::Result<()> {
404 /// let config_service = Arc::new(TestConfigService::with_defaults());
405 /// let app = App::new(config_service);
406 ///
407 /// // Match files programmatically
408 /// app.match_files("/path/to/files", true).await?; // dry_run = true
409 /// # Ok(())
410 /// # }
411 /// ```
412 ///
413 /// # Arguments
414 ///
415 /// * `input_path` - Path to the directory or file to process
416 /// * `dry_run` - Whether to perform a dry run (no actual changes)
417 ///
418 /// # Errors
419 ///
420 /// Returns an error if the match operation fails.
421 pub async fn match_files(&self, input_path: &str, dry_run: bool) -> Result<()> {
422 let args = cli::MatchArgs {
423 path: Some(input_path.into()),
424 input_paths: vec![],
425 dry_run,
426 confidence: 80,
427 recursive: false,
428 backup: false,
429 copy: false,
430 move_files: false,
431 no_extract: false,
432 };
433 self.handle_command(cli::Commands::Match(args)).await
434 }
435
436 /// Convert subtitle files programmatically.
437 ///
438 /// # Examples
439 ///
440 /// ```rust,no_run
441 /// use subx_cli::{App, config::TestConfigService};
442 /// use std::sync::Arc;
443 ///
444 /// # async fn example() -> subx_cli::Result<()> {
445 /// let config_service = Arc::new(TestConfigService::with_defaults());
446 /// let app = App::new(config_service);
447 ///
448 /// // Convert to SRT format
449 /// app.convert_files("/path/to/subtitles", "srt", Some("/output/path")).await?;
450 /// # Ok(())
451 /// # }
452 /// ```
453 ///
454 /// # Arguments
455 ///
456 /// * `input_path` - Path to subtitle files to convert
457 /// * `output_format` - Target format ("srt", "ass", "vtt", etc.)
458 /// * `output_path` - Optional output directory path
459 ///
460 /// # Errors
461 ///
462 /// Returns an error if the conversion fails.
463 pub async fn convert_files(
464 &self,
465 input_path: &str,
466 output_format: &str,
467 output_path: Option<&str>,
468 ) -> Result<()> {
469 let format = match output_format.to_lowercase().as_str() {
470 "srt" => cli::OutputSubtitleFormat::Srt,
471 "ass" => cli::OutputSubtitleFormat::Ass,
472 "vtt" => cli::OutputSubtitleFormat::Vtt,
473 "sub" => cli::OutputSubtitleFormat::Sub,
474 _ => {
475 return Err(error::SubXError::CommandExecution(format!(
476 "Unsupported output format: {output_format}. Supported formats: srt, ass, vtt, sub"
477 )));
478 }
479 };
480
481 let args = cli::ConvertArgs {
482 input: Some(input_path.into()),
483 input_paths: vec![],
484 recursive: false,
485 format: Some(format),
486 output: output_path.map(Into::into),
487 keep_original: false,
488 encoding: "utf-8".to_string(),
489 no_extract: false,
490 };
491 self.handle_command(cli::Commands::Convert(args)).await
492 }
493
494 /// Synchronize subtitle files programmatically.
495 ///
496 /// # Examples
497 ///
498 /// ```rust,no_run
499 /// use subx_cli::{App, config::TestConfigService};
500 /// use std::sync::Arc;
501 ///
502 /// # async fn example() -> subx_cli::Result<()> {
503 /// let config_service = Arc::new(TestConfigService::with_defaults());
504 /// let app = App::new(config_service);
505 ///
506 /// // Synchronize using VAD method
507 /// app.sync_files("/path/to/video.mp4", "/path/to/subtitle.srt", "vad").await?;
508 /// # Ok(())
509 /// # }
510 /// ```
511 ///
512 /// # Arguments
513 ///
514 /// * `video_path` - Path to video file for audio analysis
515 /// * `subtitle_path` - Path to subtitle file to synchronize
516 /// * `method` - Synchronization method ("vad", "manual")
517 ///
518 /// # Errors
519 ///
520 /// Returns an error if synchronization fails.
521 pub async fn sync_files(
522 &self,
523 video_path: &str,
524 subtitle_path: &str,
525 method: &str,
526 ) -> Result<()> {
527 let sync_method = match method.to_lowercase().as_str() {
528 "vad" => Some(cli::SyncMethodArg::Vad),
529 "manual" => Some(cli::SyncMethodArg::Manual),
530 _ => {
531 return Err(error::SubXError::CommandExecution(format!(
532 "Unsupported sync method: {method}. Supported methods: vad, manual"
533 )));
534 }
535 };
536
537 let args = cli::SyncArgs {
538 positional_paths: Vec::new(),
539 video: Some(video_path.into()),
540 subtitle: Some(subtitle_path.into()),
541 input_paths: vec![],
542 recursive: false,
543 offset: None,
544 method: sync_method,
545 window: 30,
546 vad_sensitivity: None,
547 output: None,
548 verbose: false,
549 dry_run: false,
550 force: false,
551 batch: None,
552 no_extract: false,
553 };
554 self.handle_command(cli::Commands::Sync(args)).await
555 }
556
557 /// Synchronize subtitle files with manual offset.
558 ///
559 /// # Examples
560 ///
561 /// ```rust,no_run
562 /// use subx_cli::{App, config::TestConfigService};
563 /// use std::sync::Arc;
564 ///
565 /// # async fn example() -> subx_cli::Result<()> {
566 /// let config_service = Arc::new(TestConfigService::with_defaults());
567 /// let app = App::new(config_service);
568 ///
569 /// // Apply +2.5 second offset to subtitles
570 /// app.sync_files_with_offset("/path/to/subtitle.srt", 2.5).await?;
571 /// # Ok(())
572 /// # }
573 /// ```
574 ///
575 /// # Arguments
576 ///
577 /// * `subtitle_path` - Path to subtitle file to synchronize
578 /// * `offset` - Time offset in seconds (positive delays, negative advances)
579 ///
580 /// # Errors
581 ///
582 /// Returns an error if synchronization fails.
583 pub async fn sync_files_with_offset(&self, subtitle_path: &str, offset: f32) -> Result<()> {
584 let args = cli::SyncArgs {
585 positional_paths: Vec::new(),
586 video: None,
587 subtitle: Some(subtitle_path.into()),
588 input_paths: vec![],
589 recursive: false,
590 offset: Some(offset),
591 method: None,
592 window: 30,
593 vad_sensitivity: None,
594 output: None,
595 verbose: false,
596 dry_run: false,
597 force: false,
598 batch: None,
599 no_extract: false,
600 };
601 self.handle_command(cli::Commands::Sync(args)).await
602 }
603
604 /// Get a reference to the configuration service.
605 ///
606 /// This allows access to the configuration service for testing or
607 /// advanced use cases.
608 pub fn config_service(&self) -> &std::sync::Arc<dyn config::ConfigService> {
609 &self.config_service
610 }
611
612 /// Get the current configuration.
613 ///
614 /// This is a convenience method that retrieves the configuration
615 /// from the configured service.
616 ///
617 /// # Errors
618 ///
619 /// Returns an error if configuration loading fails.
620 pub fn get_config(&self) -> Result<config::Config> {
621 self.config_service.get_config()
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use std::sync::Arc;
629
630 fn is_expected_test_error(e: &error::SubXError) -> bool {
631 let msg = format!("{e:?}");
632 msg.contains("NotFound")
633 || msg.contains("No subtitle files found")
634 || msg.contains("No video files found")
635 || msg.contains("Config")
636 || msg.contains("no such file")
637 || msg.contains("cannot find")
638 || msg.contains("No input")
639 || msg.contains("No files")
640 || msg.contains("FileNotFound")
641 || msg.contains("IoError")
642 || msg.contains("PathNotFound")
643 || msg.contains("InvalidInput")
644 || msg.contains("CommandExecution")
645 || msg.contains("NoInputSpecified")
646 }
647
648 #[test]
649 fn test_version_is_not_empty() {
650 assert!(!VERSION.is_empty());
651 }
652
653 #[test]
654 fn test_version_matches_cargo_pkg_version() {
655 assert_eq!(VERSION, env!("CARGO_PKG_VERSION"));
656 }
657
658 #[test]
659 fn core_version_matches_declared_requirement() {
660 // Inside the workspace, `path` resolution wins outright and the
661 // `version = "1.0"` key in the subx-core dependency declaration is
662 // never consulted. This is the only local guard against the pinned
663 // submodule drifting outside the declared caret requirement.
664 assert!(!CORE_VERSION.is_empty());
665 assert_eq!(CORE_VERSION.split('.').next(), Some("1"));
666 }
667
668 #[test]
669 fn test_app_new_stores_config_service() {
670 let config_service = Arc::new(TestConfigService::with_defaults());
671 let app = App::new(config_service.clone());
672 // Verify config_service() returns a valid Arc
673 let _ = app.config_service();
674 }
675
676 #[test]
677 fn test_app_get_config_returns_config() {
678 let config_service = Arc::new(TestConfigService::with_defaults());
679 let app = App::new(config_service);
680 let config = app.get_config().expect("get_config should succeed");
681 // Just verify we got a config object with a valid AI provider field
682 let _ = config.ai.provider;
683 }
684
685 #[test]
686 fn test_app_get_config_with_ai_settings() {
687 let config_service = Arc::new(TestConfigService::with_ai_settings("openai", "gpt-4.1"));
688 let app = App::new(config_service);
689 let config = app.get_config().expect("get_config should succeed");
690 assert_eq!(config.ai.provider, "openai");
691 assert_eq!(config.ai.model, "gpt-4.1");
692 }
693
694 #[test]
695 fn test_app_config_service_getter() {
696 let config_service = Arc::new(TestConfigService::with_defaults());
697 let app = App::new(config_service);
698 // The returned Arc should be usable (get_config succeeds)
699 let svc = app.config_service();
700 assert!(svc.get_config().is_ok());
701 }
702
703 #[tokio::test]
704 async fn test_convert_files_unknown_format_returns_error() {
705 let config_service = Arc::new(TestConfigService::with_defaults());
706 let app = App::new(config_service);
707 let result = app.convert_files("/nonexistent", "xyz_unknown", None).await;
708 assert!(result.is_err());
709 let err_msg = format!("{:?}", result.unwrap_err());
710 assert!(
711 err_msg.contains("Unsupported output format"),
712 "Error should mention unsupported format, got: {err_msg}"
713 );
714 }
715
716 #[tokio::test]
717 async fn test_convert_files_srt_format_accepted() {
718 let config_service = Arc::new(TestConfigService::with_defaults());
719 let app = App::new(config_service);
720 let result = app.convert_files("/nonexistent_path", "srt", None).await;
721 match result {
722 Ok(_) => {}
723 Err(e) => assert!(
724 is_expected_test_error(&e),
725 "Unexpected error for srt format: {e:?}"
726 ),
727 }
728 }
729
730 #[tokio::test]
731 async fn test_convert_files_ass_format_accepted() {
732 let config_service = Arc::new(TestConfigService::with_defaults());
733 let app = App::new(config_service);
734 let result = app.convert_files("/nonexistent_path", "ass", None).await;
735 match result {
736 Ok(_) => {}
737 Err(e) => assert!(
738 is_expected_test_error(&e),
739 "Unexpected error for ass format: {e:?}"
740 ),
741 }
742 }
743
744 #[tokio::test]
745 async fn test_convert_files_vtt_format_accepted() {
746 let config_service = Arc::new(TestConfigService::with_defaults());
747 let app = App::new(config_service);
748 let result = app.convert_files("/nonexistent_path", "vtt", None).await;
749 match result {
750 Ok(_) => {}
751 Err(e) => assert!(
752 is_expected_test_error(&e),
753 "Unexpected error for vtt format: {e:?}"
754 ),
755 }
756 }
757
758 #[tokio::test]
759 async fn test_convert_files_sub_format_accepted() {
760 let config_service = Arc::new(TestConfigService::with_defaults());
761 let app = App::new(config_service);
762 let result = app.convert_files("/nonexistent_path", "sub", None).await;
763 match result {
764 Ok(_) => {}
765 Err(e) => assert!(
766 is_expected_test_error(&e),
767 "Unexpected error for sub format: {e:?}"
768 ),
769 }
770 }
771
772 #[tokio::test]
773 async fn test_convert_files_format_case_insensitive() {
774 let config_service = Arc::new(TestConfigService::with_defaults());
775 let app = App::new(config_service);
776 // Uppercase format should be treated the same as lowercase
777 let result = app.convert_files("/nonexistent_path", "SRT", None).await;
778 match result {
779 Ok(_) => {}
780 Err(e) => assert!(
781 is_expected_test_error(&e),
782 "Unexpected error for uppercase SRT: {e:?}"
783 ),
784 }
785 }
786
787 #[tokio::test]
788 async fn test_sync_files_unknown_method_returns_error() {
789 let config_service = Arc::new(TestConfigService::with_defaults());
790 let app = App::new(config_service);
791 let result = app
792 .sync_files("/video.mp4", "/subtitle.srt", "unknown_method")
793 .await;
794 assert!(result.is_err());
795 let err_msg = format!("{:?}", result.unwrap_err());
796 assert!(
797 err_msg.contains("Unsupported sync method"),
798 "Error should mention unsupported method, got: {err_msg}"
799 );
800 }
801
802 #[tokio::test]
803 async fn test_sync_files_vad_method_accepted() {
804 let config_service = Arc::new(TestConfigService::with_defaults());
805 let app = App::new(config_service);
806 let result = app
807 .sync_files("/nonexistent_video.mp4", "/nonexistent_sub.srt", "vad")
808 .await;
809 match result {
810 Ok(_) => {}
811 Err(e) => assert!(
812 is_expected_test_error(&e),
813 "Unexpected error for vad method: {e:?}"
814 ),
815 }
816 }
817
818 #[tokio::test]
819 async fn test_sync_files_manual_method_accepted() {
820 let config_service = Arc::new(TestConfigService::with_defaults());
821 let app = App::new(config_service);
822 let result = app
823 .sync_files("/nonexistent_video.mp4", "/nonexistent_sub.srt", "manual")
824 .await;
825 match result {
826 Ok(_) => {}
827 Err(e) => assert!(
828 is_expected_test_error(&e),
829 "Unexpected error for manual method: {e:?}"
830 ),
831 }
832 }
833
834 #[tokio::test]
835 async fn test_sync_files_method_case_insensitive() {
836 let config_service = Arc::new(TestConfigService::with_defaults());
837 let app = App::new(config_service);
838 let result = app
839 .sync_files("/nonexistent_video.mp4", "/nonexistent_sub.srt", "VAD")
840 .await;
841 match result {
842 Ok(_) => {}
843 Err(e) => assert!(
844 is_expected_test_error(&e),
845 "Uppercase VAD should not give format error, got: {e:?}"
846 ),
847 }
848 }
849
850 #[tokio::test]
851 async fn test_sync_files_with_offset_accepted() {
852 let config_service = Arc::new(TestConfigService::with_defaults());
853 let app = App::new(config_service);
854 let result = app
855 .sync_files_with_offset("/nonexistent_sub.srt", 2.5)
856 .await;
857 match result {
858 Ok(_) => {}
859 Err(e) => assert!(
860 is_expected_test_error(&e),
861 "Unexpected error for sync with offset: {e:?}"
862 ),
863 }
864 }
865
866 #[tokio::test]
867 async fn test_sync_files_with_negative_offset() {
868 let config_service = Arc::new(TestConfigService::with_defaults());
869 let app = App::new(config_service);
870 let result = app
871 .sync_files_with_offset("/nonexistent_sub.srt", -1.5)
872 .await;
873 match result {
874 Ok(_) => {}
875 Err(e) => assert!(
876 is_expected_test_error(&e),
877 "Unexpected error for sync with negative offset: {e:?}"
878 ),
879 }
880 }
881
882 #[tokio::test]
883 async fn test_match_files_dry_run() {
884 let config_service = Arc::new(TestConfigService::with_ai_settings(
885 "test_provider",
886 "test_model",
887 ));
888 let app = App::new(config_service);
889 let result = app.match_files("/nonexistent_subx_test_path", true).await;
890 match result {
891 Ok(_) => {}
892 Err(e) => assert!(
893 is_expected_test_error(&e),
894 "Unexpected error for match dry run: {e:?}"
895 ),
896 }
897 }
898
899 #[test]
900 fn test_result_type_alias_ok() {
901 let r: Result<i32> = Ok(42);
902 assert_eq!(r.unwrap(), 42);
903 }
904
905 #[test]
906 fn test_result_type_alias_err() {
907 let r: Result<i32> = Err(error::SubXError::config("test error"));
908 assert!(r.is_err());
909 }
910}