diff --git a/README.md b/README.md new file mode 100644 index 0000000..31b04c2 --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# Yarr + +A Terminal User Interface (TUI) for managing Sonarr. + +## Features + +- View system status and health +- Browse series and episodes +- Monitor download queue +- View download history +- Interactive TUI interface +- Configurable via config files, environment variables, or CLI arguments + +## Installation + +```bash +cargo install --path . +``` + +## Configuration + +Yarr supports multiple configuration methods with the following priority order (highest to lowest): + +1. Command line arguments +2. Environment variables +3. Configuration file +4. Default values + +### Configuration File + +Create a configuration file in one of these locations: + +- `./yarr.toml` (current directory) +- `~/.config/yarr/config.toml` (user config directory) + +Example configuration: + +```toml +[sonarr] +url = "http://localhost:8989" +api_key = "your-api-key-here" +``` + +### Environment Variables + +Set these environment variables: + +```bash +export YARR_SONARR_URL="http://localhost:8989" +export YARR_SONARR_API_KEY="your-api-key-here" +``` + +### Command Line Arguments + +```bash +yarr --sonarr-url="http://localhost:8989" --sonarr-api-key="your-api-key" +``` + +## Usage + +### TUI Mode (Default) + +Launch the interactive TUI: + +```bash +yarr +# or explicitly +yarr tui +``` + +### Command Line Mode + +List all series: + +```bash +yarr list +``` + +List only monitored series: + +```bash +yarr list --monitored +``` + +Add a new series: + +```bash +yarr add --name "Series Name" +``` + +### Configuration Management + +Create a sample config file: + +```bash +yarr config init +``` + +Create config file at specific location: + +```bash +yarr config init --path /path/to/config.toml +``` + +Show current configuration: + +```bash +yarr config show +``` + +Show configuration file search paths: + +```bash +yarr config paths +``` + +### Shell Completions + +Generate shell completions: + +```bash +# Bash +yarr completions bash > /etc/bash_completion.d/yarr + +# Zsh +yarr completions zsh > ~/.zfunc/_yarr + +# Fish +yarr completions fish > ~/.config/fish/completions/yarr.fish + +# PowerShell +yarr completions powershell > yarr.ps1 +``` + +## TUI Controls + +- `q` - Quit +- `↑/↓` or `j/k` - Navigate up/down +- `Enter` - Select/expand +- `Tab` - Switch between panels +- `r` - Refresh data + +## Getting Started + +1. Install yarr +2. Create a configuration file: + ```bash + yarr config init + ``` +3. Edit the configuration file to set your Sonarr URL and API key +4. Launch the TUI: + ```bash + yarr + ``` + +## Finding Your Sonarr API Key + +1. Open your Sonarr web interface +2. Go to Settings > General +3. Find the "Security" section +4. Copy the "API Key" value + +## License + +MIT \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..36c8867 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,186 @@ +use config::{Config, ConfigError, Environment, File}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppConfig { + pub sonarr: SonarrConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SonarrConfig { + pub url: String, + pub api_key: String, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + sonarr: SonarrConfig { + url: "http://localhost:8989".to_string(), + api_key: String::new(), + }, + } + } +} + +impl AppConfig { + /// Load configuration from various sources with the following priority: + /// 1. Command line arguments (highest priority) + /// 2. Environment variables + /// 3. Config file + /// 4. Default values (lowest priority) + pub fn load( + config_path: Option, + sonarr_url: Option, + sonarr_api_key: Option, + ) -> Result { + let mut builder = Config::builder(); + + // Start with default config + builder = builder.add_source(Config::try_from(&AppConfig::default())?); + + // Add config file if it exists + if let Some(path) = config_path { + if path.exists() { + builder = builder.add_source(File::from(path)); + } + } else { + // Try to load from default locations + if let Some(config_dir) = dirs::config_dir() { + let yarr_config = config_dir.join("yarr").join("config.toml"); + if yarr_config.exists() { + builder = builder.add_source(File::from(yarr_config)); + } + } + + // Also try current directory + let local_config = std::env::current_dir() + .map(|dir| dir.join("yarr.toml")) + .unwrap_or_else(|_| PathBuf::from("yarr.toml")); + + if local_config.exists() { + builder = builder.add_source(File::from(local_config)); + } + } + + // Add environment variables with YARR_ prefix + builder = builder.add_source( + Environment::with_prefix("YARR") + .try_parsing(true) + .separator("_"), + ); + + // Override with command line arguments if provided + let mut config = builder.build()?.try_deserialize::()?; + + if let Some(url) = sonarr_url { + config.sonarr.url = url; + } + + if let Some(api_key) = sonarr_api_key { + config.sonarr.api_key = api_key; + } + + Ok(config) + } + + /// Create a sample config file + pub fn create_sample_config(path: &PathBuf) -> Result<(), Box> { + let sample_config = AppConfig { + sonarr: SonarrConfig { + url: "http://localhost:8989".to_string(), + api_key: "your-api-key-here".to_string(), + }, + }; + + let toml_content = toml::to_string_pretty(&sample_config)?; + + // Create directory if it doesn't exist + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + std::fs::write(path, toml_content)?; + Ok(()) + } + + /// Validate the configuration + pub fn validate(&self) -> Result<(), String> { + if self.sonarr.url.is_empty() { + return Err("Sonarr URL is required".to_string()); + } + + if self.sonarr.api_key.is_empty() { + return Err("Sonarr API key is required".to_string()); + } + + // Validate URL format + if !self.sonarr.url.starts_with("http://") && !self.sonarr.url.starts_with("https://") { + return Err("Sonarr URL must start with http:// or https://".to_string()); + } + + Ok(()) + } + + /// Get the default config file paths + pub fn get_default_config_paths() -> Vec { + let mut paths = Vec::new(); + + // Current directory + if let Ok(current_dir) = std::env::current_dir() { + paths.push(current_dir.join("yarr.toml")); + } + + // User config directory + if let Some(config_dir) = dirs::config_dir() { + paths.push(config_dir.join("yarr").join("config.toml")); + } + + paths + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + + #[test] + fn test_default_config() { + let config = AppConfig::default(); + assert_eq!(config.sonarr.url, "http://localhost:8989"); + assert_eq!(config.sonarr.api_key, ""); + } + + #[test] + fn test_config_validation() { + let mut config = AppConfig::default(); + + // Should fail validation with empty API key + assert!(config.validate().is_err()); + + // Should fail with invalid URL + config.sonarr.api_key = "test-key".to_string(); + config.sonarr.url = "invalid-url".to_string(); + assert!(config.validate().is_err()); + + // Should pass with valid config + config.sonarr.url = "https://example.com".to_string(); + assert!(config.validate().is_ok()); + } + + #[test] + fn test_config_override() { + // Test that CLI args override config + let config = AppConfig::load( + None, + Some("https://cli-url.com".to_string()), + Some("cli-api-key".to_string()), + ) + .unwrap(); + + assert_eq!(config.sonarr.url, "https://cli-url.com"); + assert_eq!(config.sonarr.api_key, "cli-api-key"); + } +} diff --git a/yarr.toml.example b/yarr.toml.example new file mode 100644 index 0000000..041634d --- /dev/null +++ b/yarr.toml.example @@ -0,0 +1,20 @@ +# Yarr Configuration File +# Copy this file to one of the following locations: +# - ./yarr.toml (current directory) +# - ~/.config/yarr/config.toml (user config directory) + +[sonarr] +# Sonarr server URL (required) +# Example: "http://localhost:8989" or "https://sonarr.example.com" +url = "http://localhost:8989" + +# Sonarr API key (required) +# You can find this in Sonarr under Settings > General > Security > API Key +api_key = "your-api-key-here" + +# Environment variables can also be used: +# YARR_SONARR_URL="http://localhost:8989" +# YARR_SONARR_API_KEY="your-api-key-here" +# +# Command line arguments take highest priority: +# yarr --sonarr-url="http://localhost:8989" --sonarr-api-key="your-key"