Skip to main content

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 crate::cli::InputPathHandler;
5use crate::error::SubXError;
6use clap::Args;
7use std::path::PathBuf;
8
9/// Arguments for AI-powered subtitle file matching and renaming.
10#[derive(Args, Debug)]
11pub struct MatchArgs {
12    /// Target directory path containing video and subtitle files
13    pub path: Option<PathBuf>,
14
15    /// Specify file or directory paths to process (new parameter), can be used multiple times
16    #[arg(short = 'i', long = "input", value_name = "PATH")]
17    pub input_paths: Vec<PathBuf>,
18
19    /// Enable dry-run mode to preview operations without making changes
20    #[arg(long)]
21    pub dry_run: bool,
22
23    /// Minimum confidence threshold for file matching (0-100)
24    #[arg(long, default_value = "80", value_parser = clap::value_parser!(u8).range(0..=100))]
25    pub confidence: u8,
26
27    /// Recursively process subdirectories
28    #[arg(short, long)]
29    pub recursive: bool,
30
31    /// Create backup copies of original files before renaming
32    #[arg(long)]
33    pub backup: bool,
34
35    /// Copy matched subtitle files to the same folder as their corresponding video files
36    #[arg(long, short = 'c')]
37    pub copy: bool,
38
39    /// Move matched subtitle files to the same folder as their corresponding video files
40    #[arg(long = "move", short = 'm')]
41    pub move_files: bool,
42
43    /// Disable automatic archive extraction for `-i` inputs
44    #[arg(long, default_value_t = false)]
45    pub no_extract: bool,
46}
47
48impl MatchArgs {
49    /// Validate that copy and move arguments are not used together
50    pub fn validate(&self) -> Result<(), String> {
51        if self.copy && self.move_files {
52            return Err(
53                "Cannot use --copy and --move together. Please choose one operation mode."
54                    .to_string(),
55            );
56        }
57        Ok(())
58    }
59
60    /// Get all input paths, combining path and input_paths parameters
61    pub fn get_input_handler(&self) -> Result<InputPathHandler, SubXError> {
62        let optional_paths = vec![self.path.clone()];
63        let merged_paths = InputPathHandler::merge_paths_from_multiple_sources(
64            &optional_paths,
65            &self.input_paths,
66            &[],
67        )?;
68
69        Ok(InputPathHandler::from_args(&merged_paths, self.recursive)?
70            .with_extensions(&["mp4", "mkv", "avi", "mov", "srt", "ass", "vtt", "sub"])
71            .with_no_extract(self.no_extract))
72    }
73}
74
75// Test parameter parsing behavior
76#[cfg(test)]
77mod tests {
78    use crate::cli::{Cli, Commands};
79    use clap::Parser;
80    use std::path::PathBuf;
81
82    #[test]
83    fn test_match_args_default_values() {
84        let cli = Cli::try_parse_from(&["subx-cli", "match", "path"]).unwrap();
85        let args = match cli.command {
86            Commands::Match(m) => m,
87            _ => panic!("Expected Match command"),
88        };
89        assert_eq!(args.path, Some(PathBuf::from("path")));
90        assert!(args.input_paths.is_empty());
91        assert!(!args.dry_run);
92        assert!(!args.recursive);
93        assert!(!args.backup);
94        assert_eq!(args.confidence, 80);
95    }
96
97    #[test]
98    fn test_match_args_parsing() {
99        let cli = Cli::try_parse_from(&[
100            "subx-cli",
101            "match",
102            "path",
103            "--dry-run",
104            "--recursive",
105            "--backup",
106            "--confidence",
107            "50",
108        ])
109        .unwrap();
110        let args = match cli.command {
111            Commands::Match(m) => m,
112            _ => panic!("Expected Match command"),
113        };
114        assert_eq!(args.path, Some(PathBuf::from("path")));
115        assert!(args.input_paths.is_empty());
116        assert!(args.dry_run);
117        assert!(args.recursive);
118        assert!(args.backup);
119        assert_eq!(args.confidence, 50);
120    }
121
122    #[test]
123    fn test_match_args_invalid_confidence() {
124        let res = Cli::try_parse_from(&["subx-cli", "match", "path", "--confidence", "150"]);
125        assert!(res.is_err());
126    }
127
128    #[test]
129    fn test_match_args_copy_and_move_mutually_exclusive() {
130        let cli = Cli::try_parse_from(&["subx-cli", "match", "path", "--copy", "--move"]).unwrap();
131        let args = match cli.command {
132            Commands::Match(m) => m,
133            _ => panic!("Expected Match command"),
134        };
135        let result = args.validate();
136        assert!(result.is_err());
137        assert!(
138            result
139                .unwrap_err()
140                .contains("Cannot use --copy and --move together")
141        );
142    }
143
144    #[test]
145    fn test_match_args_copy_parameter() {
146        let cli = Cli::try_parse_from(&["subx-cli", "match", "path", "--copy"]).unwrap();
147        let args = match cli.command {
148            Commands::Match(m) => m,
149            _ => panic!("Expected Match command"),
150        };
151        assert!(args.copy);
152        assert!(!args.move_files);
153        assert!(args.validate().is_ok());
154    }
155
156    #[test]
157    fn test_match_args_move_parameter() {
158        let cli = Cli::try_parse_from(&["subx-cli", "match", "path", "--move"]).unwrap();
159        let args = match cli.command {
160            Commands::Match(m) => m,
161            _ => panic!("Expected Match command"),
162        };
163        assert!(!args.copy);
164        assert!(args.move_files);
165        assert!(args.validate().is_ok());
166    }
167
168    #[test]
169    fn test_match_args_input_paths() {
170        let cli = Cli::try_parse_from(&[
171            "subx-cli",
172            "match",
173            "-i",
174            "dir1",
175            "-i",
176            "dir2",
177            "--recursive",
178        ])
179        .unwrap();
180        let args = match cli.command {
181            Commands::Match(m) => m,
182            _ => panic!("Expected Match command"),
183        };
184        assert!(args.path.is_none());
185        assert_eq!(
186            args.input_paths,
187            vec![PathBuf::from("dir1"), PathBuf::from("dir2")]
188        );
189        assert!(args.recursive);
190    }
191}