subx_cli/cli/
match_args.rs

1#![allow(clippy::needless_borrows_for_generic_args)]
2//! Command-line arguments for the AI-powered subtitle matching command.
3
4use clap::Args;
5use std::path::PathBuf;
6
7/// Arguments for AI-powered subtitle file matching and renaming.
8///
9/// This structure defines all command-line options available for the `match`
10/// subcommand, which uses artificial intelligence to analyze video and subtitle
11/// files and automatically rename subtitles to match their corresponding videos.
12///
13/// # Operation Modes
14///
15/// - **Normal Mode**: Performs actual file operations
16/// - **Dry Run Mode**: Simulates operations without making changes (`--dry-run`)
17/// - **Recursive Mode**: Processes subdirectories (`--recursive`)
18/// - **Backup Mode**: Creates backups before renaming (`--backup`)
19/// - **Copy Mode**: Copy matched subtitle files to video folders (`--copy`)
20/// - **Move Mode**: Move matched subtitle files to video folders (`--move`)
21///
22/// # AI Matching Process
23///
24/// 1. Scans the target directory for video and subtitle files
25/// 2. Extracts content samples from both file types
26/// 3. Uses AI to analyze content similarity
27/// 4. Matches files based on confidence threshold
28/// 5. Renames subtitle files to match video file names
29/// 6. Optionally relocates subtitle files to video directories
30///
31/// # Examples
32///
33/// ```bash
34/// # Basic matching in current directory
35/// subx match ./videos
36///
37/// # Dry run with high confidence threshold
38/// subx match ./videos --dry-run --confidence 90
39///
40/// # Recursive matching with backup and copy to video folders
41/// subx match ./media --recursive --backup --copy
42/// ```
43#[derive(Args, Debug)]
44pub struct MatchArgs {
45    /// Target directory path containing video and subtitle files.
46    ///
47    /// The directory should contain both video files and subtitle files
48    /// that need to be matched and renamed. Supported video formats include
49    /// MP4, MKV, AVI, etc. Supported subtitle formats include SRT, ASS, VTT, etc.
50    pub path: PathBuf,
51
52    /// Enable dry-run mode to preview operations without making changes.
53    ///
54    /// When enabled, the command will analyze files and show what operations
55    /// would be performed, but won't actually rename any files. This is useful
56    /// for testing matching accuracy before committing to changes.
57    #[arg(long)]
58    pub dry_run: bool,
59
60    /// Minimum confidence threshold for file matching (0-100).
61    ///
62    /// Only file pairs with similarity confidence above this threshold
63    /// will be matched and renamed. Higher values result in more conservative
64    /// matching with fewer false positives, while lower values are more
65    /// aggressive but may include incorrect matches.
66    ///
67    /// # Recommended Values
68    /// - 90-100: Very conservative, highest accuracy
69    /// - 80-89: Balanced approach (default)
70    /// - 70-79: More aggressive matching
71    /// - Below 70: Not recommended for automatic operations
72    #[arg(long, default_value = "80", value_parser = clap::value_parser!(u8).range(0..=100))]
73    pub confidence: u8,
74
75    /// Recursively process subdirectories.
76    ///
77    /// When enabled, the matching process will descend into subdirectories
78    /// and process video and subtitle files found there. Each subdirectory
79    /// is processed independently, so files are only matched within the
80    /// same directory level.
81    #[arg(short, long)]
82    pub recursive: bool,
83
84    /// Create backup copies of original files before renaming.
85    ///
86    /// When enabled, original subtitle files are copied to `.bak` files
87    /// before being renamed. This provides a safety net in case the
88    /// matching algorithm makes incorrect decisions.
89    #[arg(long)]
90    pub backup: bool,
91
92    /// Copy matched subtitle files to the same folder as their corresponding video files.
93    ///
94    /// When enabled along with recursive search, subtitle files that are matched
95    /// with video files in different directories will be copied to the video file's
96    /// directory. This ensures that media players can automatically load subtitles.
97    /// The original subtitle files are preserved in their original locations.
98    /// Cannot be used together with --move.
99    #[arg(long, short = 'c')]
100    pub copy: bool,
101
102    /// Move matched subtitle files to the same folder as their corresponding video files.
103    ///
104    /// When enabled along with recursive search, subtitle files that are matched
105    /// with video files in different directories will be moved to the video file's
106    /// directory. This ensures that media players can automatically load subtitles.
107    /// The original subtitle files are removed from their original locations.
108    /// Cannot be used together with --copy.
109    #[arg(long = "move", short = 'm')]
110    pub move_files: bool,
111}
112
113impl MatchArgs {
114    /// Validate that copy and move arguments are not used together
115    pub fn validate(&self) -> Result<(), String> {
116        if self.copy && self.move_files {
117            return Err(
118                "Cannot use --copy and --move together. Please choose one operation mode."
119                    .to_string(),
120            );
121        }
122        Ok(())
123    }
124}
125
126// Test parameter parsing behavior
127#[cfg(test)]
128mod tests {
129    use crate::cli::{Cli, Commands};
130    use clap::Parser;
131    use std::path::PathBuf;
132
133    #[test]
134    fn test_match_args_default_values() {
135        let cli = Cli::try_parse_from(&["subx-cli", "match", "path"]).unwrap();
136        let args = match cli.command {
137            Commands::Match(m) => m,
138            _ => panic!("Expected Match command"),
139        };
140        assert_eq!(args.path, PathBuf::from("path"));
141        assert!(!args.dry_run);
142        assert!(!args.recursive);
143        assert!(!args.backup);
144        assert_eq!(args.confidence, 80);
145    }
146
147    #[test]
148    fn test_match_args_parsing() {
149        let cli = Cli::try_parse_from(&[
150            "subx-cli",
151            "match",
152            "path",
153            "--dry-run",
154            "--recursive",
155            "--backup",
156            "--confidence",
157            "50",
158        ])
159        .unwrap();
160        let args = match cli.command {
161            Commands::Match(m) => m,
162            _ => panic!("Expected Match command"),
163        };
164        assert!(args.dry_run);
165        assert!(args.recursive);
166        assert!(args.backup);
167        assert_eq!(args.confidence, 50);
168    }
169
170    #[test]
171    fn test_match_args_invalid_confidence() {
172        let res = Cli::try_parse_from(&["subx-cli", "match", "path", "--confidence", "150"]);
173        assert!(res.is_err());
174    }
175
176    #[test]
177    fn test_match_args_copy_and_move_mutually_exclusive() {
178        let cli = Cli::try_parse_from(&["subx-cli", "match", "path", "--copy", "--move"]).unwrap();
179        let args = match cli.command {
180            Commands::Match(m) => m,
181            _ => panic!("Expected Match command"),
182        };
183        let result = args.validate();
184        assert!(result.is_err());
185        assert!(
186            result
187                .unwrap_err()
188                .contains("Cannot use --copy and --move together")
189        );
190    }
191
192    #[test]
193    fn test_match_args_copy_parameter() {
194        let cli = Cli::try_parse_from(&["subx-cli", "match", "path", "--copy"]).unwrap();
195        let args = match cli.command {
196            Commands::Match(m) => m,
197            _ => panic!("Expected Match command"),
198        };
199        assert!(args.copy);
200        assert!(!args.move_files);
201        assert!(args.validate().is_ok());
202    }
203
204    #[test]
205    fn test_match_args_move_parameter() {
206        let cli = Cli::try_parse_from(&["subx-cli", "match", "path", "--move"]).unwrap();
207        let args = match cli.command {
208            Commands::Match(m) => m,
209            _ => panic!("Expected Match command"),
210        };
211        assert!(!args.copy);
212        assert!(args.move_files);
213        assert!(args.validate().is_ok());
214    }
215}