use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; #[derive(Debug, Serialize, Deserialize, Default)] pub struct Config { #[serde(skip)] pub config_path: PathBuf, pub music_paths: Vec, pub scan_interval: Option, pub lastfm_api_key: Option, pub lastfm_api_secret: Option, pub lastfm_session_key: Option, pub listenbrainz_token: Option, pub volume: Option, pub shuffle: Option, pub display_format: Option, } impl Config { pub fn load() -> Result> { let config_dir = if cfg!(debug_assertions) { std::env::current_exe()? .parent() .map(|p| p.to_path_buf()) .unwrap_or_else(|| PathBuf::from(".")) } else { let proj_dirs = ProjectDirs::from("com", "rsplayer", "rsplayer") .ok_or("Failed to get project directories")?; if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { PathBuf::from(xdg).join("rsplayer") } else { proj_dirs.config_dir().to_path_buf() } }; let config_path = config_dir.join("config.toml"); if !config_path.exists() { fs::create_dir_all(&config_dir)?; let default = Self { config_path: config_path.clone(), music_paths: vec![dirs::audio_dir().unwrap_or_else(|| PathBuf::from("~/Music"))], scan_interval: Some(3600), lastfm_api_key: None, lastfm_api_secret: None, lastfm_session_key: None, listenbrainz_token: None, volume: Some(50), shuffle: None, display_format: Some(String::from("$ARTIST - $TRACK")), }; default.save()?; return Ok(default); } let content = fs::read_to_string(&config_path)?; let mut config: Self = toml::from_str(&content)?; config.config_path = config_path; if config.music_paths.is_empty() { config.music_paths = vec![dirs::audio_dir().unwrap_or_else(|| PathBuf::from("~/Music"))]; } if config.scan_interval.is_none() { config.scan_interval = Some(3600); } config.save()?; Ok(config) } pub fn save(&self) -> Result<(), Box> { let mut output = String::new(); let paths_str = self .music_paths .iter() .map(|p| format!("\"{}\"", p.display())) .collect::>() .join(", "); output.push_str(&format!("music_paths = [{}]\n", paths_str)); output.push_str(&format!( "scan_interval = {}\n", self.scan_interval.unwrap_or(3600) )); let api_key = self .lastfm_api_key .as_ref() .filter(|s| !s.is_empty()) .map(|s| s.as_str()) .unwrap_or(""); output.push_str(&format!("lastfm_api_key = \"{}\"\n", api_key)); let api_secret = self .lastfm_api_secret .as_ref() .filter(|s| !s.is_empty()) .map(|s| s.as_str()) .unwrap_or(""); output.push_str(&format!("lastfm_api_secret = \"{}\"\n", api_secret)); let session_key = self .lastfm_session_key .as_ref() .filter(|s| !s.is_empty()) .map(|s| s.as_str()) .unwrap_or(""); output.push_str(&format!("lastfm_session_key = \"{}\"\n", session_key)); let lb_token = self .listenbrainz_token .as_ref() .filter(|s| !s.is_empty()) .map(|s| s.as_str()) .unwrap_or(""); output.push_str(&format!("listenbrainz_token = \"{}\"\n", lb_token)); output.push_str(&format!("volume = {}\n", self.volume.unwrap_or(0))); let shuffle = self .shuffle .as_ref() .filter(|s| !s.is_empty()) .map(|s| s.as_str()) .unwrap_or(""); output.push_str(&format!("shuffle = \"{}\"\n", shuffle)); let fmt = self.display_format.clone().unwrap_or_default(); output.push_str(&format!("display_format = \"{}\"\n", fmt)); if let Some(parent) = self.config_path.parent() { fs::create_dir_all(parent)?; } fs::write(&self.config_path, output)?; Ok(()) } pub fn get(&self, key: &str) -> Result> { match key { "music_paths" => Ok(self .music_paths .iter() .map(|p| p.display().to_string()) .collect::>() .join(", ")), "scan_interval" => Ok(self .scan_interval .map(|i| i.to_string()) .unwrap_or_else(|| "not set".to_string())), "lastfm_api_key" => Ok(self .lastfm_api_key .as_ref() .filter(|s| !s.is_empty()) .map(|_| "".to_string()) .unwrap_or_else(|| "not set".to_string())), "lastfm_api_secret" => Ok(self .lastfm_api_secret .as_ref() .filter(|s| !s.is_empty()) .map(|_| "".to_string()) .unwrap_or_else(|| "not set".to_string())), "lastfm_session_key" => Ok(self .lastfm_session_key .as_ref() .filter(|s| !s.is_empty()) .map(|_| "".to_string()) .unwrap_or_else(|| "not set".to_string())), "listenbrainz_token" => Ok(self .listenbrainz_token .as_ref() .filter(|s| !s.is_empty()) .map(|_| "".to_string()) .unwrap_or_else(|| "not set".to_string())), "volume" => Ok(self .volume .map(|v| v.to_string()) .unwrap_or_else(|| "not set".to_string())), "shuffle" => Ok(self.shuffle.clone().unwrap_or_else(|| "off".to_string())), "display_format" => Ok(self .display_format .clone() .unwrap_or_else(|| "not set".to_string())), _ => Err(format!("Unknown setting: {}", key).into()), } } pub fn set(&mut self, key: &str, value: &str) -> Result<(), Box> { match key { "music_paths" => { self.music_paths = value.split(',').map(|p| PathBuf::from(p.trim())).collect(); } "scan_interval" => { self.scan_interval = Some(value.parse()?); } "lastfm_api_key" => { self.lastfm_api_key = Some(value.to_string()); } "lastfm_api_secret" => { self.lastfm_api_secret = Some(value.to_string()); } "lastfm_session_key" => { self.lastfm_session_key = Some(value.to_string()); } "listenbrainz_token" => { self.listenbrainz_token = Some(value.to_string()); } "volume" => { self.volume = Some(value.parse()?); } "shuffle" => { if value == "on" || value == "all" || value == "album" || value == "albums" { self.shuffle = Some(value.to_string()); } else if value == "off" || value == "none" { self.shuffle = None; } else { return Err(format!("Invalid shuffle value: {}", value).into()); } } "display_format" => { self.display_format = Some(value.to_string()); } _ => return Err(format!("Unknown setting: {}", key).into()), } Ok(()) } pub fn list(&self) { println!( "music_paths = {}", self.music_paths .iter() .map(|p| p.display().to_string()) .collect::>() .join(", ") ); println!( "scan_interval = {}", self.scan_interval .map_or("not set".to_string(), |v| v.to_string()) ); println!( "lastfm_api_key = {}", self.lastfm_api_key .as_ref() .filter(|s| !s.is_empty()) .map_or("not set".to_string(), |_| "".to_string()) ); println!( "lastfm_api_secret = {}", self.lastfm_api_secret .as_ref() .filter(|s| !s.is_empty()) .map_or("not set".to_string(), |_| "".to_string()) ); println!( "lastfm_session_key = {}", self.lastfm_session_key .as_ref() .filter(|s| !s.is_empty()) .map_or("not set".to_string(), |_| "".to_string()) ); println!( "listenbrainz_token = {}", self.listenbrainz_token .as_ref() .filter(|s| !s.is_empty()) .map_or("not set".to_string(), |_| "".to_string()) ); println!( "volume = {}", self.volume .filter(|&v| v != 0) .map_or("not set".to_string(), |v| v.to_string()) ); println!( "shuffle = {}", self.shuffle .clone() .filter(|s| !s.is_empty()) .unwrap_or_else(|| "off".to_string()) ); println!( "display_format = {}", self.display_format .clone() .unwrap_or_else(|| "not set".to_string()) ); } pub fn unset(&mut self, key: &str) -> Result<(), Box> { match key { "music_paths" => self.music_paths.clear(), "scan_interval" => self.scan_interval = None, "lastfm_api_key" => self.lastfm_api_key = None, "lastfm_api_secret" => self.lastfm_api_secret = None, "lastfm_session_key" => self.lastfm_session_key = None, "listenbrainz_token" => self.listenbrainz_token = None, "volume" => self.volume = None, "shuffle" => self.shuffle = None, "display_format" => self.display_format = None, _ => return Err(format!("Unknown setting: {}", key).into()), } Ok(()) } pub fn get_socket_path() -> PathBuf { let runtime_dir = std::env::var("XDG_RUNTIME_DIR") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("/tmp")); runtime_dir.join("rsplayer").join("daemon.sock") } }