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}