1use clap::{Parser, Subcommand, Args};
4use std::path::PathBuf;
5use anyhow::Result;
6
7use crate::utils::{ConfigManager, DependencyChecker};
8use crate::VERSION;
9
10#[derive(Parser)]
12#[command(name = "audiobook-forge")]
13#[command(version = VERSION)]
14#[command(about = "Convert audiobook directories to M4B format with chapters and metadata")]
15#[command(long_about = "
16Audiobook Forge is a CLI tool that converts audiobook directories containing
17MP3 files into high-quality M4B audiobook files with proper chapters and metadata.
18
19Features:
20• Automatic quality detection and preservation
21• Smart chapter generation from multiple sources
22• Parallel batch processing
23• Metadata extraction and enhancement
24• Cover art embedding
25")]
26pub struct Cli {
27 #[command(subcommand)]
28 pub command: Commands,
29
30 #[arg(global = true, short, long)]
32 pub verbose: bool,
33}
34
35#[derive(Subcommand)]
36pub enum Commands {
37 Build(BuildArgs),
39
40 Organize(OrganizeArgs),
42
43 #[command(subcommand)]
45 Config(ConfigCommands),
46
47 #[command(subcommand)]
49 Metadata(MetadataCommands),
50
51 Match(MatchArgs),
53
54 Check,
56
57 Version,
59}
60
61#[derive(Args)]
62pub struct BuildArgs {
63 #[arg(short, long)]
65 pub root: Option<PathBuf>,
66
67 #[arg(short, long)]
69 pub out: Option<PathBuf>,
70
71 #[arg(short = 'j', long, value_parser = clap::value_parser!(u8).range(1..=8))]
73 pub parallel: Option<u8>,
74
75 #[arg(long)]
77 pub skip_existing: Option<bool>,
78
79 #[arg(long)]
81 pub force: bool,
82
83 #[arg(long)]
85 pub normalize: bool,
86
87 #[arg(long)]
89 pub dry_run: bool,
90
91 #[arg(long)]
93 pub prefer_stereo: Option<bool>,
94
95 #[arg(long, value_parser = ["auto", "files", "cue", "id3", "none"])]
97 pub chapter_source: Option<String>,
98
99 #[arg(long)]
101 pub cover_names: Option<String>,
102
103 #[arg(long)]
105 pub language: Option<String>,
106
107 #[arg(long)]
109 pub keep_temp: bool,
110
111 #[arg(long)]
113 pub delete_originals: bool,
114
115 #[arg(long)]
117 pub use_apple_silicon_encoder: Option<bool>,
118
119 #[arg(long)]
121 pub fetch_audible: bool,
122
123 #[arg(long)]
125 pub audible_region: Option<String>,
126
127 #[arg(long)]
129 pub audible_auto_match: bool,
130
131 #[arg(long)]
133 pub config: Option<PathBuf>,
134}
135
136#[derive(Args)]
137pub struct OrganizeArgs {
138 #[arg(short, long)]
140 pub root: Option<PathBuf>,
141
142 #[arg(long)]
144 pub dry_run: bool,
145
146 #[arg(long)]
148 pub config: Option<PathBuf>,
149}
150
151#[derive(Subcommand)]
152pub enum ConfigCommands {
153 Init {
155 #[arg(long)]
157 force: bool,
158 },
159
160 Show {
162 #[arg(long)]
164 config: Option<PathBuf>,
165 },
166
167 Validate {
169 #[arg(long)]
171 config: Option<PathBuf>,
172 },
173
174 Path,
176
177 Edit,
179}
180
181#[derive(Subcommand)]
182pub enum MetadataCommands {
183 Fetch {
185 #[arg(long)]
187 asin: Option<String>,
188
189 #[arg(long)]
191 title: Option<String>,
192
193 #[arg(long)]
195 author: Option<String>,
196
197 #[arg(long, default_value = "us")]
199 region: String,
200
201 #[arg(long)]
203 output: Option<PathBuf>,
204 },
205
206 Enrich {
208 #[arg(long)]
210 file: PathBuf,
211
212 #[arg(long)]
214 asin: Option<String>,
215
216 #[arg(long)]
218 auto_detect: bool,
219
220 #[arg(long, default_value = "us")]
222 region: String,
223 },
224}
225
226#[derive(Args)]
228pub struct MatchArgs {
229 #[arg(long, short = 'f', conflicts_with = "dir")]
231 pub file: Option<PathBuf>,
232
233 #[arg(long, short = 'd', conflicts_with = "file")]
235 pub dir: Option<PathBuf>,
236
237 #[arg(long)]
239 pub title: Option<String>,
240
241 #[arg(long)]
243 pub author: Option<String>,
244
245 #[arg(long)]
247 pub auto: bool,
248
249 #[arg(long, default_value = "us")]
251 pub region: String,
252
253 #[arg(long)]
255 pub keep_cover: bool,
256
257 #[arg(long)]
259 pub dry_run: bool,
260}
261
262pub fn run() -> Result<()> {
264 let cli = Cli::parse();
265
266 let log_level = if cli.verbose { "debug" } else { "info" };
268 tracing_subscriber::fmt()
269 .with_env_filter(
270 tracing_subscriber::EnvFilter::try_from_default_env()
271 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(log_level))
272 )
273 .init();
274
275 match cli.command {
277 Commands::Build(args) => run_build(args),
278 Commands::Organize(args) => run_organize(args),
279 Commands::Config(cmd) => run_config(cmd),
280 Commands::Metadata(cmd) => run_metadata(cmd),
281 Commands::Match(args) => run_match(args),
282 Commands::Check => run_check(),
283 Commands::Version => run_version(),
284 }
285}
286
287fn run_build(args: BuildArgs) -> Result<()> {
288 println!("Build command - Phase 2-4 implementation");
289 println!("Args: root={:?}, out={:?}, parallel={:?}",
290 args.root, args.out, args.parallel);
291
292 anyhow::bail!("Build command not yet implemented. Coming in Phase 2!");
294}
295
296fn run_organize(args: OrganizeArgs) -> Result<()> {
297 println!("Organize command - Phase 5 implementation");
298 println!("Args: root={:?}, dry_run={}",
299 args.root, args.dry_run);
300
301 anyhow::bail!("Organize command not yet implemented. Coming in Phase 5!");
303}
304
305fn run_config(cmd: ConfigCommands) -> Result<()> {
306 match cmd {
307 ConfigCommands::Init { force } => {
308 let path = ConfigManager::init(force)?;
309 println!("✓ Config file created at: {}", path.display());
310 println!("\nEdit the file to customize your settings:");
311 println!(" audiobook-forge config edit");
312 Ok(())
313 }
314
315 ConfigCommands::Show { config } => {
316 let yaml = ConfigManager::show(config.as_ref())?;
317 println!("{}", yaml);
318 Ok(())
319 }
320
321 ConfigCommands::Validate { config } => {
322 let cfg = ConfigManager::load_or_default(config.as_ref())?;
323 let warnings = ConfigManager::validate(&cfg)?;
324
325 if warnings.is_empty() {
326 println!("✓ Configuration is valid");
327 } else {
328 println!("⚠ Configuration warnings:");
329 for warning in warnings {
330 println!(" • {}", warning);
331 }
332 }
333 Ok(())
334 }
335
336 ConfigCommands::Path => {
337 let path = ConfigManager::default_config_path()?;
338 println!("{}", path.display());
339 Ok(())
340 }
341
342 ConfigCommands::Edit => {
343 let path = ConfigManager::default_config_path()?;
344
345 if !path.exists() {
347 ConfigManager::init(false)?;
348 }
349
350 let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vim".to_string());
352 let status = std::process::Command::new(&editor)
353 .arg(&path)
354 .status()?;
355
356 if !status.success() {
357 anyhow::bail!("Editor exited with non-zero status");
358 }
359
360 Ok(())
361 }
362 }
363}
364
365fn run_check() -> Result<()> {
366 println!("Audiobook Forge v{}", VERSION);
367 println!("\nChecking system dependencies...\n");
368
369 let deps = DependencyChecker::check_all();
370 let all_met = deps.iter().all(|d| d.found);
371
372 for dep in &deps {
373 println!("{}", dep);
374 }
375
376 if all_met {
377 println!("\n✓ All dependencies are installed");
378
379 if std::env::consts::OS == "macos" {
381 if DependencyChecker::check_aac_at_support() {
382 println!("✓ Apple Silicon encoder (aac_at) is available");
383 } else {
384 println!("ℹ Apple Silicon encoder (aac_at) not available");
385 }
386 }
387
388 Ok(())
389 } else {
390 println!("\n✗ Some dependencies are missing");
391 println!("\nInstallation instructions:");
392 println!(" macOS: brew install ffmpeg atomicparsley gpac");
393 println!(" Ubuntu: apt install ffmpeg atomicparsley gpac");
394 println!(" Arch: pacman -S ffmpeg atomicparsley gpac");
395
396 anyhow::bail!("Missing required dependencies");
397 }
398}
399
400fn run_metadata(cmd: MetadataCommands) -> Result<()> {
401 use crate::cli::handlers::handle_metadata;
402 use crate::utils::ConfigManager;
403
404 let config = ConfigManager::load_or_default(None)?;
406
407 let runtime = tokio::runtime::Runtime::new()?;
409 runtime.block_on(handle_metadata(cmd, config))
410}
411
412fn run_match(args: MatchArgs) -> Result<()> {
413 use crate::cli::handlers::handle_match;
414 use crate::utils::ConfigManager;
415
416 let config = ConfigManager::load_or_default(None)?;
418
419 let runtime = tokio::runtime::Runtime::new()?;
421 runtime.block_on(handle_match(args, config))
422}
423
424fn run_version() -> Result<()> {
425 println!("Audiobook Forge v{}", VERSION);
426 println!("Rust rewrite - High-performance audiobook processing");
427 Ok(())
428}