use anyhow::{Context, Result}; use clap::Parser; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; #[derive(Parser, Debug, Clone)] #[command(name = "emby-client", about = "TUI Emby client with libmpv playback")] pub struct CliArgs { /// Emby server URL (e.g. http://emby.home.local) #[arg(long, env = "EMBY_SERVER")] pub server: Option, /// Emby username #[arg(short, long, env = "EMBY_USER")] pub user: Option, /// Emby password #[arg(short, long, env = "EMBY_PASSWORD")] pub password: Option, /// Path to config file. Defaults: ./config.toml then $XDG_CONFIG_HOME/emby-client/config.toml #[arg(short, long)] pub config: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct FileConfig { pub server: Option, pub user: Option, pub password: Option, } #[derive(Debug, Clone)] pub struct Config { pub server: String, pub user: String, pub password: String, } /// Returns `(Some(config), save_target)` when all credentials are available, /// or `(None, save_target)` when at least one is missing — the caller can then /// prompt the user. `save_target` is the path where new credentials should /// be persisted if the user chooses to save them. pub fn load(args: &CliArgs) -> Result<(Option, Option)> { let (file_cfg, save_target) = read_file_config(args.config.as_deref())?; let server = args.server.clone().or(file_cfg.server); let user = args.user.clone().or(file_cfg.user); let password = args.password.clone().or(file_cfg.password); let cfg = match (server, user, password) { (Some(server), Some(user), Some(password)) => Some(Config { server: server.trim_end_matches('/').to_string(), user, password, }), _ => None, }; Ok((cfg, Some(save_target))) } fn read_file_config(explicit: Option<&Path>) -> Result<(FileConfig, PathBuf)> { let default_target = default_save_path(); let candidates: Vec = if let Some(p) = explicit { vec![p.to_path_buf()] } else { let mut v = vec![PathBuf::from("config.toml")]; if let Some(dirs) = directories::ProjectDirs::from("", "", "emby-client") { v.push(dirs.config_dir().join("config.toml")); } v }; let mut save_target = explicit .map(|p| p.to_path_buf()) .unwrap_or(default_target); for path in &candidates { if path.exists() { let text = std::fs::read_to_string(path) .with_context(|| format!("reading config {}", path.display()))?; let cfg: FileConfig = toml::from_str(&text) .with_context(|| format!("parsing config {}", path.display()))?; save_target = path.clone(); return Ok((cfg, save_target)); } } Ok((FileConfig::default(), save_target)) } fn default_save_path() -> PathBuf { if let Some(dirs) = directories::ProjectDirs::from("", "", "emby-client") { dirs.config_dir().join("config.toml") } else { PathBuf::from("config.toml") } } pub fn save(path: &Path, cfg: &Config) -> Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("creating {}", parent.display()))?; } let file_cfg = FileConfig { server: Some(cfg.server.clone()), user: Some(cfg.user.clone()), password: Some(cfg.password.clone()), }; let text = toml::to_string_pretty(&file_cfg).context("serializing config")?; std::fs::write(path, text).with_context(|| format!("writing {}", path.display()))?; Ok(()) }