subx_cli/cli/convert_args.rs
1//! Subtitle format conversion command-line arguments and options.
2//!
3//! This module defines the command-line interface for the `convert` subcommand,
4//! which handles subtitle format conversion between different standards like SRT,
5//! ASS, VTT, and SUB. It provides comprehensive format conversion capabilities
6//! with encoding support and optional file preservation.
7//!
8//! # Supported Formats
9//!
10//! - **SRT (SubRip)**: Most widely used subtitle format
11//! - **ASS (Advanced SubStation Alpha)**: Rich formatting and styling support
12//! - **VTT (WebVTT)**: Web-optimized subtitle format for HTML5 video
13//! - **SUB (MicroDVD/SubViewer)**: Frame-based subtitle format
14//!
15//! # Examples
16//!
17//! ```bash
18//! # Convert SRT to ASS format
19//! subx convert input.srt --format ass --output output.ass
20//!
21//! # Batch convert all SRT files in a directory to VTT
22//! subx convert ./subtitles/ --format vtt
23//!
24//! # Convert with specific encoding
25//! subx convert input.srt --format ass --encoding utf-8 --keep-original
26//! ```
27
28#![allow(clippy::needless_borrows_for_generic_args)]
29// src/cli/convert_args.rs
30use crate::cli::InputPathHandler;
31use crate::error::SubXError;
32use clap::{Args, ValueEnum};
33use std::path::PathBuf;
34
35/// Command-line arguments for subtitle format conversion.
36#[derive(Args, Debug)]
37pub struct ConvertArgs {
38 /// Input file or directory path containing subtitle files
39 pub input: Option<PathBuf>,
40
41 /// Specify file or directory paths to process (new parameter), can be used multiple times
42 #[arg(short = 'i', long = "input", value_name = "PATH")]
43 pub input_paths: Vec<PathBuf>,
44
45 /// Recursively process subdirectories (new parameter)
46 #[arg(short, long)]
47 pub recursive: bool,
48
49 /// Target output format for converted subtitles
50 #[arg(long, value_enum)]
51 pub format: Option<OutputSubtitleFormat>,
52
53 /// Output file path for the converted subtitle
54 #[arg(short, long)]
55 pub output: Option<PathBuf>,
56
57 /// Preserve the original files after conversion
58 #[arg(long)]
59 pub keep_original: bool,
60
61 /// Character encoding for input and output files
62 #[arg(long, default_value = "utf-8")]
63 pub encoding: String,
64
65 /// Disable automatic archive extraction for `-i` inputs
66 #[arg(long, default_value_t = false)]
67 pub no_extract: bool,
68}
69
70impl ConvertArgs {
71 /// Get all input paths
72 /// Get all input paths, combining input and input_paths parameters
73 pub fn get_input_handler(&self) -> Result<InputPathHandler, SubXError> {
74 let optional_paths = vec![self.input.clone()];
75 let merged_paths = InputPathHandler::merge_paths_from_multiple_sources(
76 &optional_paths,
77 &self.input_paths,
78 &[],
79 )?;
80
81 Ok(InputPathHandler::from_args(&merged_paths, self.recursive)?
82 .with_extensions(&["srt", "ass", "vtt", "sub", "ssa"])
83 .with_no_extract(self.no_extract))
84 }
85}
86
87/// Supported output subtitle formats for conversion operations.
88///
89/// This enum defines all subtitle formats that SubX can generate as output.
90/// Each format has specific characteristics and use cases:
91///
92/// - **SRT**: Simple, widely supported, good for basic subtitles
93/// - **ASS**: Advanced formatting, styling, and positioning capabilities
94/// - **VTT**: Web-optimized, supports HTML5 video elements
95/// - **SUB**: Frame-based timing, used in some legacy systems
96///
97/// # Format Characteristics
98///
99/// | Format | Timing | Styling | Web Support | Compatibility |
100/// |--------|--------|---------|-------------|---------------|
101/// | SRT | Time | Basic | Good | Excellent |
102/// | ASS | Time | Rich | Limited | Good |
103/// | VTT | Time | Medium | Excellent | Good |
104/// | SUB | Frame | Basic | Poor | Limited |
105///
106/// # Examples
107///
108/// ```rust
109/// use subx_cli::cli::OutputSubtitleFormat;
110///
111/// let srt_format = OutputSubtitleFormat::Srt;
112/// assert_eq!(srt_format.as_str(), "srt");
113/// assert_eq!(srt_format.file_extension(), ".srt");
114/// ```
115#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
116pub enum OutputSubtitleFormat {
117 /// SubRip (.srt) format - most widely supported subtitle format.
118 ///
119 /// Features:
120 /// - Simple time-based format
121 /// - Basic text formatting (bold, italic, underline)
122 /// - Excellent player compatibility
123 /// - Small file size
124 Srt,
125
126 /// Advanced SubStation Alpha (.ass) format - professional subtitle format.
127 ///
128 /// Features:
129 /// - Rich styling and formatting options
130 /// - Precise positioning and animation
131 /// - Multiple font and color support
132 /// - Advanced timing controls
133 Ass,
134
135 /// WebVTT (.vtt) format - web-optimized subtitle format.
136 ///
137 /// Features:
138 /// - HTML5 video element support
139 /// - CSS-like styling capabilities
140 /// - Cue positioning and alignment
141 /// - Web accessibility features
142 Vtt,
143
144 /// MicroDVD/SubViewer (.sub) format - frame-based subtitle format.
145 ///
146 /// Features:
147 /// - Frame-based timing (not time-based)
148 /// - Basic text formatting
149 /// - Legacy format support
150 /// - Compact file structure
151 Sub,
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::cli::{Cli, Commands};
158 use clap::Parser;
159 use std::path::PathBuf;
160
161 #[test]
162 fn test_convert_args_default_and_format() {
163 let cli =
164 Cli::try_parse_from(&["subx-cli", "convert", "movie.srt", "--format", "ass"]).unwrap();
165 let args = match cli.command {
166 Commands::Convert(a) => a,
167 _ => panic!("Expected Convert command"),
168 };
169 assert!(args.input_paths.is_empty());
170 assert_eq!(args.input, Some(PathBuf::from("movie.srt")));
171 assert!(!args.recursive);
172 assert_eq!(args.format, Some(OutputSubtitleFormat::Ass));
173 assert!(!args.keep_original);
174 assert_eq!(args.encoding, "utf-8");
175 }
176
177 #[test]
178 fn test_convert_args_multiple_input_recursive_and_keep_original() {
179 let cli = Cli::try_parse_from(&[
180 "subx-cli",
181 "convert",
182 "-i",
183 "d1",
184 "-i",
185 "f.srt",
186 "--recursive",
187 "--format",
188 "vtt",
189 "--encoding",
190 "utf-16",
191 "--keep-original",
192 ])
193 .unwrap();
194 let args = match cli.command {
195 Commands::Convert(a) => a,
196 _ => panic!("Expected Convert command"),
197 };
198 assert_eq!(
199 args.input_paths,
200 vec![PathBuf::from("d1"), PathBuf::from("f.srt")]
201 );
202 assert_eq!(args.input, None);
203 assert!(args.recursive);
204 assert_eq!(args.format, Some(OutputSubtitleFormat::Vtt));
205 assert!(args.keep_original);
206 assert_eq!(args.encoding, "utf-16");
207 }
208}
209
210impl OutputSubtitleFormat {
211 /// Returns the format identifier as a string.
212 ///
213 /// This method provides the lowercase string representation of the format,
214 /// which is used for command-line arguments and configuration files.
215 ///
216 /// # Examples
217 ///
218 /// ```rust
219 /// use subx_cli::cli::OutputSubtitleFormat;
220 ///
221 /// assert_eq!(OutputSubtitleFormat::Srt.as_str(), "srt");
222 /// assert_eq!(OutputSubtitleFormat::Ass.as_str(), "ass");
223 /// assert_eq!(OutputSubtitleFormat::Vtt.as_str(), "vtt");
224 /// assert_eq!(OutputSubtitleFormat::Sub.as_str(), "sub");
225 /// ```
226 pub fn as_str(&self) -> &'static str {
227 match self {
228 OutputSubtitleFormat::Srt => "srt",
229 OutputSubtitleFormat::Ass => "ass",
230 OutputSubtitleFormat::Vtt => "vtt",
231 OutputSubtitleFormat::Sub => "sub",
232 }
233 }
234
235 /// Returns the file extension for this format including the dot prefix.
236 ///
237 /// This method provides the standard file extension used for each
238 /// subtitle format, which is useful for generating output filenames.
239 ///
240 /// # Examples
241 ///
242 /// ```rust
243 /// use subx_cli::cli::OutputSubtitleFormat;
244 ///
245 /// assert_eq!(OutputSubtitleFormat::Srt.file_extension(), ".srt");
246 /// assert_eq!(OutputSubtitleFormat::Ass.file_extension(), ".ass");
247 /// assert_eq!(OutputSubtitleFormat::Vtt.file_extension(), ".vtt");
248 /// assert_eq!(OutputSubtitleFormat::Sub.file_extension(), ".sub");
249 /// ```
250 pub fn file_extension(&self) -> &'static str {
251 match self {
252 OutputSubtitleFormat::Srt => ".srt",
253 OutputSubtitleFormat::Ass => ".ass",
254 OutputSubtitleFormat::Vtt => ".vtt",
255 OutputSubtitleFormat::Sub => ".sub",
256 }
257 }
258}
259
260impl std::fmt::Display for OutputSubtitleFormat {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 write!(f, "{}", self.as_str())
263 }
264}
265// Test parameter parsing behavior
266#[cfg(test)]
267mod tests_parse {
268 use super::*;
269 use crate::cli::{Cli, Commands};
270 use clap::Parser;
271 use std::path::PathBuf;
272
273 #[test]
274 fn test_convert_args_default_values() {
275 let cli = Cli::try_parse_from(&["subx-cli", "convert", "in_path"]).unwrap();
276 let args = match cli.command {
277 Commands::Convert(c) => c,
278 _ => panic!("Expected Convert command"),
279 };
280 assert_eq!(args.input, Some(PathBuf::from("in_path")));
281 assert!(args.input_paths.is_empty());
282 assert!(!args.recursive);
283 assert_eq!(args.format, None);
284 assert_eq!(args.output, None);
285 assert!(!args.keep_original);
286 assert_eq!(args.encoding, "utf-8");
287 }
288
289 #[test]
290 fn test_convert_args_parsing() {
291 let cli = Cli::try_parse_from(&[
292 "subx-cli",
293 "convert",
294 "in",
295 "--format",
296 "vtt",
297 "--output",
298 "out",
299 "--keep-original",
300 "--encoding",
301 "gbk",
302 ])
303 .unwrap();
304 let args = match cli.command {
305 Commands::Convert(c) => c,
306 _ => panic!("Expected Convert command"),
307 };
308 assert_eq!(args.input, Some(PathBuf::from("in")));
309 assert!(args.input_paths.is_empty());
310 assert!(!args.recursive);
311 assert_eq!(args.format.unwrap(), OutputSubtitleFormat::Vtt);
312 assert_eq!(args.output, Some(PathBuf::from("out")));
313 assert!(args.keep_original);
314 assert_eq!(args.encoding, "gbk");
315 }
316}