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
66impl ConvertArgs {
67    /// Get all input paths
68    /// Get all input paths, combining input and input_paths parameters
69    pub fn get_input_handler(&self) -> Result<InputPathHandler, SubXError> {
70        let optional_paths = vec![self.input.clone()];
71        let merged_paths = InputPathHandler::merge_paths_from_multiple_sources(
72            &optional_paths,
73            &self.input_paths,
74            &[],
75        )?;
76
77        Ok(InputPathHandler::from_args(&merged_paths, self.recursive)?
78            .with_extensions(&["srt", "ass", "vtt", "sub", "ssa"]))
79    }
80}
81
82/// Supported output subtitle formats for conversion operations.
83///
84/// This enum defines all subtitle formats that SubX can generate as output.
85/// Each format has specific characteristics and use cases:
86///
87/// - **SRT**: Simple, widely supported, good for basic subtitles
88/// - **ASS**: Advanced formatting, styling, and positioning capabilities
89/// - **VTT**: Web-optimized, supports HTML5 video elements
90/// - **SUB**: Frame-based timing, used in some legacy systems
91///
92/// # Format Characteristics
93///
94/// | Format | Timing | Styling | Web Support | Compatibility |
95/// |--------|--------|---------|-------------|---------------|
96/// | SRT    | Time   | Basic   | Good        | Excellent     |
97/// | ASS    | Time   | Rich    | Limited     | Good          |
98/// | VTT    | Time   | Medium  | Excellent   | Good          |
99/// | SUB    | Frame  | Basic   | Poor        | Limited       |
100///
101/// # Examples
102///
103/// ```rust
104/// use subx_cli::cli::OutputSubtitleFormat;
105///
106/// let srt_format = OutputSubtitleFormat::Srt;
107/// assert_eq!(srt_format.as_str(), "srt");
108/// assert_eq!(srt_format.file_extension(), ".srt");
109/// ```
110#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)]
111pub enum OutputSubtitleFormat {
112    /// SubRip (.srt) format - most widely supported subtitle format.
113    ///
114    /// Features:
115    /// - Simple time-based format
116    /// - Basic text formatting (bold, italic, underline)
117    /// - Excellent player compatibility
118    /// - Small file size
119    Srt,
120
121    /// Advanced SubStation Alpha (.ass) format - professional subtitle format.
122    ///
123    /// Features:
124    /// - Rich styling and formatting options
125    /// - Precise positioning and animation
126    /// - Multiple font and color support
127    /// - Advanced timing controls
128    Ass,
129
130    /// WebVTT (.vtt) format - web-optimized subtitle format.
131    ///
132    /// Features:
133    /// - HTML5 video element support
134    /// - CSS-like styling capabilities
135    /// - Cue positioning and alignment
136    /// - Web accessibility features
137    Vtt,
138
139    /// MicroDVD/SubViewer (.sub) format - frame-based subtitle format.
140    ///
141    /// Features:
142    /// - Frame-based timing (not time-based)
143    /// - Basic text formatting
144    /// - Legacy format support
145    /// - Compact file structure
146    Sub,
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::cli::{Cli, Commands};
153    use clap::Parser;
154    use std::path::PathBuf;
155
156    #[test]
157    fn test_convert_args_default_and_format() {
158        let cli =
159            Cli::try_parse_from(&["subx-cli", "convert", "movie.srt", "--format", "ass"]).unwrap();
160        let args = match cli.command {
161            Commands::Convert(a) => a,
162            _ => panic!("Expected Convert command"),
163        };
164        assert!(args.input_paths.is_empty());
165        assert_eq!(args.input, Some(PathBuf::from("movie.srt")));
166        assert!(!args.recursive);
167        assert_eq!(args.format, Some(OutputSubtitleFormat::Ass));
168        assert!(!args.keep_original);
169        assert_eq!(args.encoding, "utf-8");
170    }
171
172    #[test]
173    fn test_convert_args_multiple_input_recursive_and_keep_original() {
174        let cli = Cli::try_parse_from(&[
175            "subx-cli",
176            "convert",
177            "-i",
178            "d1",
179            "-i",
180            "f.srt",
181            "--recursive",
182            "--format",
183            "vtt",
184            "--encoding",
185            "utf-16",
186            "--keep-original",
187        ])
188        .unwrap();
189        let args = match cli.command {
190            Commands::Convert(a) => a,
191            _ => panic!("Expected Convert command"),
192        };
193        assert_eq!(
194            args.input_paths,
195            vec![PathBuf::from("d1"), PathBuf::from("f.srt")]
196        );
197        assert_eq!(args.input, None);
198        assert!(args.recursive);
199        assert_eq!(args.format, Some(OutputSubtitleFormat::Vtt));
200        assert!(args.keep_original);
201        assert_eq!(args.encoding, "utf-16");
202    }
203}
204
205impl OutputSubtitleFormat {
206    /// Returns the format identifier as a string.
207    ///
208    /// This method provides the lowercase string representation of the format,
209    /// which is used for command-line arguments and configuration files.
210    ///
211    /// # Examples
212    ///
213    /// ```rust
214    /// use subx_cli::cli::OutputSubtitleFormat;
215    ///
216    /// assert_eq!(OutputSubtitleFormat::Srt.as_str(), "srt");
217    /// assert_eq!(OutputSubtitleFormat::Ass.as_str(), "ass");
218    /// assert_eq!(OutputSubtitleFormat::Vtt.as_str(), "vtt");
219    /// assert_eq!(OutputSubtitleFormat::Sub.as_str(), "sub");
220    /// ```
221    pub fn as_str(&self) -> &'static str {
222        match self {
223            OutputSubtitleFormat::Srt => "srt",
224            OutputSubtitleFormat::Ass => "ass",
225            OutputSubtitleFormat::Vtt => "vtt",
226            OutputSubtitleFormat::Sub => "sub",
227        }
228    }
229
230    /// Returns the file extension for this format including the dot prefix.
231    ///
232    /// This method provides the standard file extension used for each
233    /// subtitle format, which is useful for generating output filenames.
234    ///
235    /// # Examples
236    ///
237    /// ```rust
238    /// use subx_cli::cli::OutputSubtitleFormat;
239    ///
240    /// assert_eq!(OutputSubtitleFormat::Srt.file_extension(), ".srt");
241    /// assert_eq!(OutputSubtitleFormat::Ass.file_extension(), ".ass");
242    /// assert_eq!(OutputSubtitleFormat::Vtt.file_extension(), ".vtt");
243    /// assert_eq!(OutputSubtitleFormat::Sub.file_extension(), ".sub");
244    /// ```
245    pub fn file_extension(&self) -> &'static str {
246        match self {
247            OutputSubtitleFormat::Srt => ".srt",
248            OutputSubtitleFormat::Ass => ".ass",
249            OutputSubtitleFormat::Vtt => ".vtt",
250            OutputSubtitleFormat::Sub => ".sub",
251        }
252    }
253}
254
255impl std::fmt::Display for OutputSubtitleFormat {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        write!(f, "{}", self.as_str())
258    }
259}
260// Test parameter parsing behavior
261#[cfg(test)]
262mod tests_parse {
263    use super::*;
264    use crate::cli::{Cli, Commands};
265    use clap::Parser;
266    use std::path::PathBuf;
267
268    #[test]
269    fn test_convert_args_default_values() {
270        let cli = Cli::try_parse_from(&["subx-cli", "convert", "in_path"]).unwrap();
271        let args = match cli.command {
272            Commands::Convert(c) => c,
273            _ => panic!("Expected Convert command"),
274        };
275        assert_eq!(args.input, Some(PathBuf::from("in_path")));
276        assert!(args.input_paths.is_empty());
277        assert!(!args.recursive);
278        assert_eq!(args.format, None);
279        assert_eq!(args.output, None);
280        assert!(!args.keep_original);
281        assert_eq!(args.encoding, "utf-8");
282    }
283
284    #[test]
285    fn test_convert_args_parsing() {
286        let cli = Cli::try_parse_from(&[
287            "subx-cli",
288            "convert",
289            "in",
290            "--format",
291            "vtt",
292            "--output",
293            "out",
294            "--keep-original",
295            "--encoding",
296            "gbk",
297        ])
298        .unwrap();
299        let args = match cli.command {
300            Commands::Convert(c) => c,
301            _ => panic!("Expected Convert command"),
302        };
303        assert_eq!(args.input, Some(PathBuf::from("in")));
304        assert!(args.input_paths.is_empty());
305        assert!(!args.recursive);
306        assert_eq!(args.format.unwrap(), OutputSubtitleFormat::Vtt);
307        assert_eq!(args.output, Some(PathBuf::from("out")));
308        assert!(args.keep_original);
309        assert_eq!(args.encoding, "gbk");
310    }
311}