feat(cli): add config management and enhanced command options

- Introduce `config` command for config file management
- Add global options for Sonarr URL and API key
- Implement `--monitored` filter for listing series
- Add default TUI startup behavior
This commit is contained in:
uttarayan21
2025-10-08 15:44:36 +05:30
parent 160ca86da5
commit 1be1e19c43
4 changed files with 795 additions and 49 deletions

View File

@@ -1,30 +1,87 @@
#[derive(Debug, clap::Parser)]
use clap::{Args, Parser, Subcommand};
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(name = "yarr")]
#[command(about = "A TUI for managing Sonarr")]
#[command(version)]
pub struct Cli {
#[clap(subcommand)]
pub cmd: SubCommand,
/// Path to config file
#[arg(short, long, global = true)]
pub config: Option<PathBuf>,
/// Sonarr URL (overrides config file)
#[arg(long, global = true, env = "YARR_SONARR_URL")]
pub sonarr_url: Option<String>,
/// Sonarr API key (overrides config file)
#[arg(long, global = true, env = "YARR_SONARR_API_KEY")]
pub sonarr_api_key: Option<String>,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Debug, clap::Subcommand)]
pub enum SubCommand {
#[clap(name = "add")]
Add(Add),
#[clap(name = "list")]
List(List),
#[clap(name = "completions")]
Completions { shell: clap_complete::Shell },
#[derive(Debug, Subcommand)]
pub enum Commands {
/// Add a new series
Add(AddArgs),
/// List series
List(ListArgs),
/// Start the TUI interface (default)
Tui,
/// Generate shell completions
Completions {
/// The shell to generate completions for
#[arg(value_enum)]
shell: clap_complete::Shell,
},
/// Configuration management
Config(ConfigArgs),
}
#[derive(Debug, clap::Args)]
pub struct Add {
#[clap(short, long)]
#[derive(Debug, Args)]
pub struct AddArgs {
/// Name of the series to add
#[arg(short, long)]
pub name: String,
}
#[derive(Debug, clap::Args)]
pub struct List {}
#[derive(Debug, Args)]
pub struct ListArgs {
/// Show only monitored series
#[arg(short, long)]
pub monitored: bool,
}
#[derive(Debug, Args)]
pub struct ConfigArgs {
#[command(subcommand)]
pub action: ConfigAction,
}
#[derive(Debug, Subcommand)]
pub enum ConfigAction {
/// Show current configuration
Show,
/// Create a sample config file
Init {
/// Path where to create the config file
#[arg(short, long)]
path: Option<PathBuf>,
},
/// Show possible config file locations
Paths,
}
impl Cli {
pub fn completions(shell: clap_complete::Shell) {
pub fn generate_completions(shell: clap_complete::Shell) {
let mut command = <Cli as clap::CommandFactory>::command();
clap_complete::generate(
shell,

View File

@@ -1,28 +1,171 @@
mod api;
mod cli;
mod config;
mod errors;
mod tui;
use crate::api::SonarrClient;
use crate::config::AppConfig;
use clap::Parser;
use std::process;
#[tokio::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
// let args = <cli::Cli as clap::Parser>::parse();
// match args.cmd {
// cli::SubCommand::Add(add) => {
// println!("Add: {:?}", add);
// }
// cli::SubCommand::List(list) => {
// println!("List: {:?}", list);
// }
// cli::SubCommand::Completions { shell } => {
// cli::Cli::completions(shell);
// }
// }
// Parse command line arguments
let args = cli::Cli::parse();
// Handle config subcommands first
if let Some(cli::Commands::Config(config_cmd)) = &args.command {
handle_config_command(config_cmd, &args)?;
return Ok(());
}
// Handle completions
if let Some(cli::Commands::Completions { shell }) = &args.command {
cli::Cli::generate_completions(*shell);
return Ok(());
}
// Load configuration
let config = match AppConfig::load(
args.config.clone(),
args.sonarr_url.clone(),
args.sonarr_api_key.clone(),
) {
Ok(config) => config,
Err(e) => {
eprintln!("Error loading configuration: {}", e);
eprintln!("Run 'yarr config init' to create a sample config file");
process::exit(1);
}
};
// Validate configuration
if let Err(e) = config.validate() {
eprintln!("Configuration error: {}", e);
eprintln!("Run 'yarr config show' to see current configuration");
eprintln!("Run 'yarr config init' to create a sample config file");
process::exit(1);
}
// Create Sonarr client
let client = SonarrClient::new(config.sonarr.url.clone(), config.sonarr.api_key.clone());
// Handle different commands
match args.command {
Some(cli::Commands::Add(add_args)) => {
handle_add_command(&client, add_args).await?;
}
Some(cli::Commands::List(list_args)) => {
handle_list_command(&client, list_args).await?;
}
Some(cli::Commands::Tui) | None => {
// Default to TUI mode
tui::run_app(client).await?;
}
Some(cli::Commands::Completions { .. }) | Some(cli::Commands::Config(_)) => {
// Already handled above
}
}
Ok(())
}
fn handle_config_command(
config_cmd: &cli::ConfigArgs,
args: &cli::Cli,
) -> Result<(), Box<dyn std::error::Error>> {
match &config_cmd.action {
cli::ConfigAction::Show => {
// Load and display current configuration
let config = AppConfig::load(
args.config.clone(),
args.sonarr_url.clone(),
args.sonarr_api_key.clone(),
)?;
println!("Current configuration:");
println!("Sonarr URL: {}", config.sonarr.url);
println!(
"Sonarr API Key: {}",
if config.sonarr.api_key.is_empty() {
"(not set)"
} else {
"***masked***"
}
);
}
cli::ConfigAction::Init { path } => {
let config_path = if let Some(path) = path {
path.clone()
} else {
// Use default config location
let paths = AppConfig::get_default_config_paths();
if let Some(default_path) = paths.get(1) {
// Prefer user config directory over current directory
default_path.clone()
} else {
std::env::current_dir()?.join("yarr.toml")
}
};
AppConfig::create_sample_config(&config_path)?;
println!("Sample configuration created at: {}", config_path.display());
println!("Please edit the file to set your Sonarr URL and API key.");
}
cli::ConfigAction::Paths => {
println!("Configuration file search order:");
let paths = AppConfig::get_default_config_paths();
for (i, path) in paths.iter().enumerate() {
let exists = if path.exists() { " (exists)" } else { "" };
println!(" {}. {}{}", i + 1, path.display(), exists);
}
println!("\nEnvironment variables:");
println!(" YARR_SONARR_URL");
println!(" YARR_SONARR_API_KEY");
}
}
Ok(())
}
async fn handle_add_command(
_client: &SonarrClient,
_add_args: cli::AddArgs,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Add command not yet implemented");
// TODO: Implement series addition
Ok(())
}
async fn handle_list_command(
client: &SonarrClient,
list_args: cli::ListArgs,
) -> Result<(), Box<dyn std::error::Error>> {
println!("Fetching series list...");
match client.get_series().await {
Ok(series_list) => {
let filtered_series: Vec<_> = if list_args.monitored {
series_list.into_iter().filter(|s| s.monitored).collect()
} else {
series_list
};
if filtered_series.is_empty() {
println!("No series found");
} else {
println!("\nSeries ({}):", filtered_series.len());
for series in filtered_series {
let status = if series.monitored { "" } else { "" };
let title = series.title.as_deref().unwrap_or("Unknown");
println!(" {} {} ({})", status, title, series.status);
}
}
}
Err(e) => {
eprintln!("Failed to fetch series: {}", e);
process::exit(1);
}
}
let client = SonarrClient::new(
"https://sonarr.tsuba.darksailor.dev".into(),
"1a47401731bf44ae9787dfcd4bab402f".into(),
);
tui::run_app(client).await?;
Ok(())
}