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, /// Preferred audio language(s), comma-separated ISO 639 codes (e.g. "jpn,eng"). /// Forwarded to mpv's `alang`. #[arg(long, env = "EMBY_AUDIO_LANG")] pub audio_lang: Option, /// Preferred subtitle language(s), comma-separated ISO 639 codes (e.g. "eng,jpn"). /// Forwarded to mpv's `slang`. #[arg(long, env = "EMBY_SUBTITLE_LANG")] pub subtitle_lang: Option, /// mpv video output driver (e.g. `gpu`, `gpu-next`, `sixel`, `kitty`, /// `drm`, `tct`). Forwarded to mpv's `vo`. Leave unset for mpv's default. #[arg(long, env = "EMBY_VIDEO_OUTPUT")] pub video_output: Option, /// Path to config file. Defaults: ./config.toml then $XDG_CONFIG_HOME/emby-client/config.toml #[arg(short, long)] pub config: Option, /// Install a desktop entry for the current executable and exit #[arg(long)] pub install_desktop: bool, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct FileConfig { pub server: Option, pub user: Option, pub password: Option, pub audio_lang: Option, pub subtitle_lang: Option, pub video_output: Option, } #[derive(Debug, Clone)] pub struct Config { pub server: String, pub user: String, pub password: String, pub audio_lang: Option, pub subtitle_lang: Option, /// mpv `vo` driver, e.g. `gpu`, `sixel`, `kitty`, `drm`, `tct`. `None` /// means we don't touch the property and mpv picks its default. pub video_output: Option, } 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 audio_lang = args.audio_lang.clone().or(file_cfg.audio_lang); let subtitle_lang = args.subtitle_lang.clone().or(file_cfg.subtitle_lang); let video_output = args.video_output.clone().or(file_cfg.video_output); let cfg = match (server, user, password) { (Some(server), Some(user), Some(password)) => Some(Config { server: server.trim_end_matches('/').to_string(), user, password, audio_lang, subtitle_lang, video_output, }), _ => 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()), audio_lang: cfg.audio_lang.clone(), subtitle_lang: cfg.subtitle_lang.clone(), video_output: cfg.video_output.clone(), }; let text = toml::to_string_pretty(&file_cfg).context("serializing config")?; std::fs::write(path, text).with_context(|| format!("writing {}", path.display()))?; Ok(()) }