diff options
| author | salmonik <raisersoft@proton.me> | 2026-05-11 16:25:49 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-05-11 16:25:49 +0200 |
| commit | 455ec54613230f4004a24f8d2798fb0084e00cb4 (patch) | |
| tree | 5ffb7606a11ebf33ea940f068ed046bfda0f0c54 /src | |
| parent | Fix shuffle to not shuffle current track - only shuffle tracks after current ... (diff) | |
| download | rsplayer-455ec54613230f4004a24f8d2798fb0084e00cb4.tar.gz rsplayer-455ec54613230f4004a24f8d2798fb0084e00cb4.zip | |
Add display_format config, fix restart command, clean up config code
- Add display_format option with get/set/list/unset for customizing track display
- Add Track::format() supporting $ARTIST, $ALBUM, $TRACK variables
- Update queue, status, notifications to use display_format
- Fix restart command using wrong subcommand 'start' -> 'start-daemon'
- Remove socket_path from config (now computed at runtime from XDG_RUNTIME_DIR)
- Consolidate duplicate save() and save_all() into single save()
- Fix redundant .clone().clone() in config.rs
- Add todo item for caching config in daemon hot paths
Diffstat (limited to '')
| -rw-r--r-- | src/cli.rs | 12 | ||||
| -rw-r--r-- | src/config.rs | 186 | ||||
| -rw-r--r-- | src/daemon.rs | 66 | ||||
| -rw-r--r-- | src/notification.rs | 23 | ||||
| -rw-r--r-- | src/queue.rs | 16 |
5 files changed, 237 insertions, 66 deletions
| @@ -14,9 +14,9 @@ pub struct Cli { | |||
| 14 | #[derive(Subcommand)] | 14 | #[derive(Subcommand)] |
| 15 | pub enum Commands { | 15 | pub enum Commands { |
| 16 | /// Start the daemon | 16 | /// Start the daemon |
| 17 | Start, | 17 | StartDaemon, |
| 18 | /// Stop the daemon | 18 | /// Stop the daemon |
| 19 | Stop, | 19 | StopDaemon, |
| 20 | /// Restart the daemon (kill existing and start new) | 20 | /// Restart the daemon (kill existing and start new) |
| 21 | Restart, | 21 | Restart, |
| 22 | /// Play a track, album, or artist | 22 | /// Play a track, album, or artist |
| @@ -37,7 +37,7 @@ pub enum Commands { | |||
| 37 | /// Toggle play/pause | 37 | /// Toggle play/pause |
| 38 | Toggle, | 38 | Toggle, |
| 39 | /// Stop playback | 39 | /// Stop playback |
| 40 | StopPlayback, | 40 | Stop, |
| 41 | /// Skip to next track | 41 | /// Skip to next track |
| 42 | Next, | 42 | Next, |
| 43 | /// Skip to previous track | 43 | /// Skip to previous track |
| @@ -101,10 +101,10 @@ pub enum SettingsCommands { | |||
| 101 | impl Cli { | 101 | impl Cli { |
| 102 | pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> { | 102 | pub fn run(&self) -> Result<(), Box<dyn std::error::Error>> { |
| 103 | match &self.command { | 103 | match &self.command { |
| 104 | Commands::Start => { | 104 | Commands::StartDaemon => { |
| 105 | crate::daemon::start_daemon()?; | 105 | crate::daemon::start_daemon()?; |
| 106 | } | 106 | } |
| 107 | Commands::Stop => { | 107 | Commands::StopDaemon => { |
| 108 | crate::daemon::stop_daemon()?; | 108 | crate::daemon::stop_daemon()?; |
| 109 | } | 109 | } |
| 110 | Commands::Restart => { | 110 | Commands::Restart => { |
| @@ -135,7 +135,7 @@ impl Cli { | |||
| 135 | Commands::Toggle => { | 135 | Commands::Toggle => { |
| 136 | crate::daemon::send_command("toggle".to_string())?; | 136 | crate::daemon::send_command("toggle".to_string())?; |
| 137 | } | 137 | } |
| 138 | Commands::StopPlayback => { | 138 | Commands::Stop => { |
| 139 | crate::daemon::send_command("stop".to_string())?; | 139 | crate::daemon::send_command("stop".to_string())?; |
| 140 | } | 140 | } |
| 141 | Commands::Next => { | 141 | Commands::Next => { |
diff --git a/src/config.rs b/src/config.rs index 42573ed..5fd2f1b 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -9,13 +9,13 @@ pub struct Config { | |||
| 9 | pub config_path: PathBuf, | 9 | pub config_path: PathBuf, |
| 10 | pub music_paths: Vec<PathBuf>, | 10 | pub music_paths: Vec<PathBuf>, |
| 11 | pub scan_interval: Option<u64>, | 11 | pub scan_interval: Option<u64>, |
| 12 | pub socket_path: Option<PathBuf>, | ||
| 13 | pub lastfm_api_key: Option<String>, | 12 | pub lastfm_api_key: Option<String>, |
| 14 | pub lastfm_api_secret: Option<String>, | 13 | pub lastfm_api_secret: Option<String>, |
| 15 | pub lastfm_session_key: Option<String>, | 14 | pub lastfm_session_key: Option<String>, |
| 16 | pub listenbrainz_token: Option<String>, | 15 | pub listenbrainz_token: Option<String>, |
| 17 | pub volume: Option<u8>, | 16 | pub volume: Option<u8>, |
| 18 | pub shuffle: Option<String>, | 17 | pub shuffle: Option<String>, |
| 18 | pub display_format: Option<String>, | ||
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | impl Config { | 21 | impl Config { |
| @@ -43,8 +43,13 @@ impl Config { | |||
| 43 | config_path: config_path.clone(), | 43 | config_path: config_path.clone(), |
| 44 | music_paths: vec![dirs::audio_dir().unwrap_or_else(|| PathBuf::from("~/Music"))], | 44 | music_paths: vec![dirs::audio_dir().unwrap_or_else(|| PathBuf::from("~/Music"))], |
| 45 | scan_interval: Some(3600), | 45 | scan_interval: Some(3600), |
| 46 | socket_path: None, | 46 | lastfm_api_key: None, |
| 47 | ..Default::default() | 47 | lastfm_api_secret: None, |
| 48 | lastfm_session_key: None, | ||
| 49 | listenbrainz_token: None, | ||
| 50 | volume: Some(50), | ||
| 51 | shuffle: None, | ||
| 52 | display_format: Some(String::from("$ARTIST - $TRACK")), | ||
| 48 | }; | 53 | }; |
| 49 | default.save()?; | 54 | default.save()?; |
| 50 | return Ok(default); | 55 | return Ok(default); |
| @@ -53,15 +58,72 @@ impl Config { | |||
| 53 | let content = fs::read_to_string(&config_path)?; | 58 | let content = fs::read_to_string(&config_path)?; |
| 54 | let mut config: Self = toml::from_str(&content)?; | 59 | let mut config: Self = toml::from_str(&content)?; |
| 55 | config.config_path = config_path; | 60 | config.config_path = config_path; |
| 61 | if config.music_paths.is_empty() { | ||
| 62 | config.music_paths = | ||
| 63 | vec![dirs::audio_dir().unwrap_or_else(|| PathBuf::from("~/Music"))]; | ||
| 64 | } | ||
| 65 | if config.scan_interval.is_none() { | ||
| 66 | config.scan_interval = Some(3600); | ||
| 67 | } | ||
| 68 | config.save()?; | ||
| 56 | Ok(config) | 69 | Ok(config) |
| 57 | } | 70 | } |
| 58 | 71 | ||
| 59 | pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> { | 72 | pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> { |
| 60 | let content = toml::to_string_pretty(self)?; | 73 | let mut output = String::new(); |
| 74 | let paths_str = self | ||
| 75 | .music_paths | ||
| 76 | .iter() | ||
| 77 | .map(|p| format!("\"{}\"", p.display())) | ||
| 78 | .collect::<Vec<_>>() | ||
| 79 | .join(", "); | ||
| 80 | output.push_str(&format!("music_paths = [{}]\n", paths_str)); | ||
| 81 | output.push_str(&format!( | ||
| 82 | "scan_interval = {}\n", | ||
| 83 | self.scan_interval.unwrap_or(3600) | ||
| 84 | )); | ||
| 85 | let api_key = self | ||
| 86 | .lastfm_api_key | ||
| 87 | .as_ref() | ||
| 88 | .filter(|s| !s.is_empty()) | ||
| 89 | .map(|s| s.as_str()) | ||
| 90 | .unwrap_or(""); | ||
| 91 | output.push_str(&format!("lastfm_api_key = \"{}\"\n", api_key)); | ||
| 92 | let api_secret = self | ||
| 93 | .lastfm_api_secret | ||
| 94 | .as_ref() | ||
| 95 | .filter(|s| !s.is_empty()) | ||
| 96 | .map(|s| s.as_str()) | ||
| 97 | .unwrap_or(""); | ||
| 98 | output.push_str(&format!("lastfm_api_secret = \"{}\"\n", api_secret)); | ||
| 99 | let session_key = self | ||
| 100 | .lastfm_session_key | ||
| 101 | .as_ref() | ||
| 102 | .filter(|s| !s.is_empty()) | ||
| 103 | .map(|s| s.as_str()) | ||
| 104 | .unwrap_or(""); | ||
| 105 | output.push_str(&format!("lastfm_session_key = \"{}\"\n", session_key)); | ||
| 106 | let lb_token = self | ||
| 107 | .listenbrainz_token | ||
| 108 | .as_ref() | ||
| 109 | .filter(|s| !s.is_empty()) | ||
| 110 | .map(|s| s.as_str()) | ||
| 111 | .unwrap_or(""); | ||
| 112 | output.push_str(&format!("listenbrainz_token = \"{}\"\n", lb_token)); | ||
| 113 | output.push_str(&format!("volume = {}\n", self.volume.unwrap_or(0))); | ||
| 114 | let shuffle = self | ||
| 115 | .shuffle | ||
| 116 | .as_ref() | ||
| 117 | .filter(|s| !s.is_empty()) | ||
| 118 | .map(|s| s.as_str()) | ||
| 119 | .unwrap_or(""); | ||
| 120 | output.push_str(&format!("shuffle = \"{}\"\n", shuffle)); | ||
| 121 | let fmt = self.display_format.clone().unwrap_or_default(); | ||
| 122 | output.push_str(&format!("display_format = \"{}\"\n", fmt)); | ||
| 61 | if let Some(parent) = self.config_path.parent() { | 123 | if let Some(parent) = self.config_path.parent() { |
| 62 | fs::create_dir_all(parent)?; | 124 | fs::create_dir_all(parent)?; |
| 63 | } | 125 | } |
| 64 | fs::write(&self.config_path, content)?; | 126 | fs::write(&self.config_path, output)?; |
| 65 | Ok(()) | 127 | Ok(()) |
| 66 | } | 128 | } |
| 67 | 129 | ||
| @@ -77,32 +139,39 @@ impl Config { | |||
| 77 | .scan_interval | 139 | .scan_interval |
| 78 | .map(|i| i.to_string()) | 140 | .map(|i| i.to_string()) |
| 79 | .unwrap_or_else(|| "not set".to_string())), | 141 | .unwrap_or_else(|| "not set".to_string())), |
| 80 | "socket_path" => Ok(self | ||
| 81 | .socket_path | ||
| 82 | .as_ref() | ||
| 83 | .map(|p| p.display().to_string()) | ||
| 84 | .unwrap_or_else(|| "default".to_string())), | ||
| 85 | "lastfm_api_key" => Ok(self | 142 | "lastfm_api_key" => Ok(self |
| 86 | .lastfm_api_key | 143 | .lastfm_api_key |
| 87 | .clone() | 144 | .as_ref() |
| 145 | .filter(|s| !s.is_empty()) | ||
| 146 | .map(|_| "<set>".to_string()) | ||
| 88 | .unwrap_or_else(|| "not set".to_string())), | 147 | .unwrap_or_else(|| "not set".to_string())), |
| 89 | "lastfm_api_secret" => Ok(self | 148 | "lastfm_api_secret" => Ok(self |
| 90 | .lastfm_api_secret | 149 | .lastfm_api_secret |
| 91 | .clone() | 150 | .as_ref() |
| 151 | .filter(|s| !s.is_empty()) | ||
| 152 | .map(|_| "<set>".to_string()) | ||
| 92 | .unwrap_or_else(|| "not set".to_string())), | 153 | .unwrap_or_else(|| "not set".to_string())), |
| 93 | "lastfm_session_key" => Ok(self | 154 | "lastfm_session_key" => Ok(self |
| 94 | .lastfm_session_key | 155 | .lastfm_session_key |
| 95 | .clone() | 156 | .as_ref() |
| 157 | .filter(|s| !s.is_empty()) | ||
| 158 | .map(|_| "<set>".to_string()) | ||
| 96 | .unwrap_or_else(|| "not set".to_string())), | 159 | .unwrap_or_else(|| "not set".to_string())), |
| 97 | "listenbrainz_token" => Ok(self | 160 | "listenbrainz_token" => Ok(self |
| 98 | .listenbrainz_token | 161 | .listenbrainz_token |
| 99 | .clone() | 162 | .as_ref() |
| 163 | .filter(|s| !s.is_empty()) | ||
| 164 | .map(|_| "<set>".to_string()) | ||
| 100 | .unwrap_or_else(|| "not set".to_string())), | 165 | .unwrap_or_else(|| "not set".to_string())), |
| 101 | "volume" => Ok(self | 166 | "volume" => Ok(self |
| 102 | .volume | 167 | .volume |
| 103 | .map(|v| v.to_string()) | 168 | .map(|v| v.to_string()) |
| 104 | .unwrap_or_else(|| "not set".to_string())), | 169 | .unwrap_or_else(|| "not set".to_string())), |
| 105 | "shuffle" => Ok(self.shuffle.clone().unwrap_or_else(|| "off".to_string())), | 170 | "shuffle" => Ok(self.shuffle.clone().unwrap_or_else(|| "off".to_string())), |
| 171 | "display_format" => Ok(self | ||
| 172 | .display_format | ||
| 173 | .clone() | ||
| 174 | .unwrap_or_else(|| "not set".to_string())), | ||
| 106 | _ => Err(format!("Unknown setting: {}", key).into()), | 175 | _ => Err(format!("Unknown setting: {}", key).into()), |
| 107 | } | 176 | } |
| 108 | } | 177 | } |
| @@ -115,9 +184,6 @@ impl Config { | |||
| 115 | "scan_interval" => { | 184 | "scan_interval" => { |
| 116 | self.scan_interval = Some(value.parse()?); | 185 | self.scan_interval = Some(value.parse()?); |
| 117 | } | 186 | } |
| 118 | "socket_path" => { | ||
| 119 | self.socket_path = Some(PathBuf::from(value)); | ||
| 120 | } | ||
| 121 | "lastfm_api_key" => { | 187 | "lastfm_api_key" => { |
| 122 | self.lastfm_api_key = Some(value.to_string()); | 188 | self.lastfm_api_key = Some(value.to_string()); |
| 123 | } | 189 | } |
| @@ -142,45 +208,97 @@ impl Config { | |||
| 142 | return Err(format!("Invalid shuffle value: {}", value).into()); | 208 | return Err(format!("Invalid shuffle value: {}", value).into()); |
| 143 | } | 209 | } |
| 144 | } | 210 | } |
| 211 | "display_format" => { | ||
| 212 | self.display_format = Some(value.to_string()); | ||
| 213 | } | ||
| 145 | _ => return Err(format!("Unknown setting: {}", key).into()), | 214 | _ => return Err(format!("Unknown setting: {}", key).into()), |
| 146 | } | 215 | } |
| 147 | Ok(()) | 216 | Ok(()) |
| 148 | } | 217 | } |
| 149 | 218 | ||
| 150 | pub fn list(&self) { | 219 | pub fn list(&self) { |
| 151 | println!("music_paths = {:?}", self.music_paths); | 220 | println!( |
| 152 | println!("scan_interval = {:?}", self.scan_interval); | 221 | "music_paths = {}", |
| 153 | println!("socket_path = {:?}", self.socket_path); | 222 | self.music_paths |
| 154 | println!("lastfm_api_key = {:?}", self.lastfm_api_key); | 223 | .iter() |
| 155 | println!("lastfm_api_secret = {:?}", self.lastfm_api_secret); | 224 | .map(|p| p.display().to_string()) |
| 156 | println!("lastfm_session_key = {:?}", self.lastfm_session_key); | 225 | .collect::<Vec<_>>() |
| 157 | println!("listenbrainz_token = {:?}", self.listenbrainz_token); | 226 | .join(", ") |
| 158 | println!("volume = {:?}", self.volume); | 227 | ); |
| 159 | println!("shuffle = {:?}", self.shuffle); | 228 | println!( |
| 229 | "scan_interval = {}", | ||
| 230 | self.scan_interval | ||
| 231 | .map_or("not set".to_string(), |v| v.to_string()) | ||
| 232 | ); | ||
| 233 | println!( | ||
| 234 | "lastfm_api_key = {}", | ||
| 235 | self.lastfm_api_key | ||
| 236 | .as_ref() | ||
| 237 | .filter(|s| !s.is_empty()) | ||
| 238 | .map_or("not set".to_string(), |_| "<set>".to_string()) | ||
| 239 | ); | ||
| 240 | println!( | ||
| 241 | "lastfm_api_secret = {}", | ||
| 242 | self.lastfm_api_secret | ||
| 243 | .as_ref() | ||
| 244 | .filter(|s| !s.is_empty()) | ||
| 245 | .map_or("not set".to_string(), |_| "<set>".to_string()) | ||
| 246 | ); | ||
| 247 | println!( | ||
| 248 | "lastfm_session_key = {}", | ||
| 249 | self.lastfm_session_key | ||
| 250 | .as_ref() | ||
| 251 | .filter(|s| !s.is_empty()) | ||
| 252 | .map_or("not set".to_string(), |_| "<set>".to_string()) | ||
| 253 | ); | ||
| 254 | println!( | ||
| 255 | "listenbrainz_token = {}", | ||
| 256 | self.listenbrainz_token | ||
| 257 | .as_ref() | ||
| 258 | .filter(|s| !s.is_empty()) | ||
| 259 | .map_or("not set".to_string(), |_| "<set>".to_string()) | ||
| 260 | ); | ||
| 261 | println!( | ||
| 262 | "volume = {}", | ||
| 263 | self.volume | ||
| 264 | .filter(|&v| v != 0) | ||
| 265 | .map_or("not set".to_string(), |v| v.to_string()) | ||
| 266 | ); | ||
| 267 | println!( | ||
| 268 | "shuffle = {}", | ||
| 269 | self.shuffle | ||
| 270 | .clone() | ||
| 271 | .filter(|s| !s.is_empty()) | ||
| 272 | .unwrap_or_else(|| "off".to_string()) | ||
| 273 | ); | ||
| 274 | println!( | ||
| 275 | "display_format = {}", | ||
| 276 | self.display_format | ||
| 277 | .clone() | ||
| 278 | .unwrap_or_else(|| "not set".to_string()) | ||
| 279 | ); | ||
| 160 | } | 280 | } |
| 161 | 281 | ||
| 162 | pub fn unset(&mut self, key: &str) -> Result<(), Box<dyn std::error::Error>> { | 282 | pub fn unset(&mut self, key: &str) -> Result<(), Box<dyn std::error::Error>> { |
| 163 | match key { | 283 | match key { |
| 164 | "music_paths" => self.music_paths.clear(), | 284 | "music_paths" => self.music_paths.clear(), |
| 165 | "scan_interval" => self.scan_interval = None, | 285 | "scan_interval" => self.scan_interval = None, |
| 166 | "socket_path" => self.socket_path = None, | ||
| 167 | "lastfm_api_key" => self.lastfm_api_key = None, | 286 | "lastfm_api_key" => self.lastfm_api_key = None, |
| 168 | "lastfm_api_secret" => self.lastfm_api_secret = None, | 287 | "lastfm_api_secret" => self.lastfm_api_secret = None, |
| 169 | "lastfm_session_key" => self.lastfm_session_key = None, | 288 | "lastfm_session_key" => self.lastfm_session_key = None, |
| 170 | "listenbrainz_token" => self.listenbrainz_token = None, | 289 | "listenbrainz_token" => self.listenbrainz_token = None, |
| 171 | "volume" => self.volume = None, | 290 | "volume" => self.volume = None, |
| 172 | "shuffle" => self.shuffle = None, | 291 | "shuffle" => self.shuffle = None, |
| 292 | "display_format" => self.display_format = None, | ||
| 173 | _ => return Err(format!("Unknown setting: {}", key).into()), | 293 | _ => return Err(format!("Unknown setting: {}", key).into()), |
| 174 | } | 294 | } |
| 175 | Ok(()) | 295 | Ok(()) |
| 176 | } | 296 | } |
| 177 | 297 | ||
| 178 | pub fn get_socket_path(&self) -> PathBuf { | 298 | pub fn get_socket_path() -> PathBuf { |
| 179 | self.socket_path.clone().unwrap_or_else(|| { | 299 | let runtime_dir = std::env::var("XDG_RUNTIME_DIR") |
| 180 | let runtime_dir = std::env::var("XDG_RUNTIME_DIR") | 300 | .map(PathBuf::from) |
| 181 | .map(PathBuf::from) | 301 | .unwrap_or_else(|_| PathBuf::from("/tmp")); |
| 182 | .unwrap_or_else(|_| PathBuf::from("/tmp")); | 302 | runtime_dir.join("rsplayer").join("daemon.sock") |
| 183 | runtime_dir.join("rsplayer").join("daemon.sock") | ||
| 184 | }) | ||
| 185 | } | 303 | } |
| 186 | } | 304 | } |
diff --git a/src/daemon.rs b/src/daemon.rs index b79e67a..acc7729 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -12,7 +12,7 @@ use std::time::Duration; | |||
| 12 | 12 | ||
| 13 | pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | 13 | pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { |
| 14 | let config = Config::load()?; | 14 | let config = Config::load()?; |
| 15 | let socket_path = config.get_socket_path(); | 15 | let socket_path = Config::get_socket_path(); |
| 16 | let pid_path = get_pid_path(&config); | 16 | let pid_path = get_pid_path(&config); |
| 17 | 17 | ||
| 18 | // Check if daemon is already running | 18 | // Check if daemon is already running |
| @@ -55,7 +55,6 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 55 | let queue = Arc::new(Mutex::new(Queue::new())); | 55 | let queue = Arc::new(Mutex::new(Queue::new())); |
| 56 | 56 | ||
| 57 | // Load and apply saved settings | 57 | // Load and apply saved settings |
| 58 | let config = Config::load().unwrap_or_default(); | ||
| 59 | if let Some(vol) = config.volume { | 58 | if let Some(vol) = config.volume { |
| 60 | audio_engine.set_volume(vol as f32 / 100.0); | 59 | audio_engine.set_volume(vol as f32 / 100.0); |
| 61 | } | 60 | } |
| @@ -69,6 +68,9 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 69 | } | 68 | } |
| 70 | } | 69 | } |
| 71 | 70 | ||
| 71 | // Save full config | ||
| 72 | config.save()?; | ||
| 73 | |||
| 72 | // Accept connections until shutdown | 74 | // Accept connections until shutdown |
| 73 | listener.set_nonblocking(true)?; | 75 | listener.set_nonblocking(true)?; |
| 74 | // Helper: poll until a specific track actually starts playing or timeout | 76 | // Helper: poll until a specific track actually starts playing or timeout |
| @@ -106,7 +108,8 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 106 | audio_engine.play(next_track.clone()); | 108 | audio_engine.play(next_track.clone()); |
| 107 | if wait_for_start(&audio_engine, &next_track, 500) { | 109 | if wait_for_start(&audio_engine, &next_track, 500) { |
| 108 | started = true; | 110 | started = true; |
| 109 | notification::notify_track(&Track::from_path(next_track)); | 111 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 112 | notification::notify_track(&Track::from_path(next_track), fmt.as_deref()); | ||
| 110 | break; | 113 | break; |
| 111 | } | 114 | } |
| 112 | } else { | 115 | } else { |
| @@ -182,7 +185,7 @@ pub fn stop_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 182 | } | 185 | } |
| 183 | 186 | ||
| 184 | // Clean up files | 187 | // Clean up files |
| 185 | let socket_path = config.get_socket_path(); | 188 | let socket_path = Config::get_socket_path(); |
| 186 | std::fs::remove_file(&socket_path).ok(); | 189 | std::fs::remove_file(&socket_path).ok(); |
| 187 | std::fs::remove_file(&pid_path).ok(); | 190 | std::fs::remove_file(&pid_path).ok(); |
| 188 | 191 | ||
| @@ -203,7 +206,7 @@ pub fn restart_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 203 | // Start new daemon in background | 206 | // Start new daemon in background |
| 204 | let current_exe = std::env::current_exe()?; | 207 | let current_exe = std::env::current_exe()?; |
| 205 | std::process::Command::new(current_exe) | 208 | std::process::Command::new(current_exe) |
| 206 | .arg("start") | 209 | .arg("start-daemon") |
| 207 | .spawn()?; | 210 | .spawn()?; |
| 208 | 211 | ||
| 209 | println!("Daemon restarted"); | 212 | println!("Daemon restarted"); |
| @@ -216,8 +219,7 @@ pub fn send_command(cmd: String) -> Result<(), Box<dyn std::error::Error>> { | |||
| 216 | } | 219 | } |
| 217 | 220 | ||
| 218 | fn send_command_raw(cmd: String) -> Result<(), Box<dyn std::error::Error>> { | 221 | fn send_command_raw(cmd: String) -> Result<(), Box<dyn std::error::Error>> { |
| 219 | let config = Config::load()?; | 222 | let socket_path = Config::get_socket_path(); |
| 220 | let socket_path = config.get_socket_path(); | ||
| 221 | 223 | ||
| 222 | let mut stream = UnixStream::connect(socket_path)?; | 224 | let mut stream = UnixStream::connect(socket_path)?; |
| 223 | stream.write_all(cmd.as_bytes())?; | 225 | stream.write_all(cmd.as_bytes())?; |
| @@ -254,7 +256,11 @@ fn process_command( | |||
| 254 | if audio.is_playing() { | 256 | if audio.is_playing() { |
| 255 | if let Some(track_path) = audio.current_track() { | 257 | if let Some(track_path) = audio.current_track() { |
| 256 | let track = Track::from_path(track_path); | 258 | let track = Track::from_path(track_path); |
| 257 | let track_str = track.display_string(0, 1); | 259 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 260 | let track_str = match fmt.as_deref() { | ||
| 261 | Some(f) => track.format(f), | ||
| 262 | None => track.display_string(0, 1), | ||
| 263 | }; | ||
| 258 | format!("Playing\nTrack: {}\n", track_str) | 264 | format!("Playing\nTrack: {}\n", track_str) |
| 259 | } else { | 265 | } else { |
| 260 | "Playing\n".to_string() | 266 | "Playing\n".to_string() |
| @@ -296,7 +302,11 @@ fn process_command( | |||
| 296 | if let Ok(mut q) = queue.lock() { | 302 | if let Ok(mut q) = queue.lock() { |
| 297 | if let Some(next_track) = q.next() { | 303 | if let Some(next_track) = q.next() { |
| 298 | audio.play(next_track.clone()); | 304 | audio.play(next_track.clone()); |
| 299 | notification::notify_track(&Track::from_path(next_track)); | 305 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 306 | notification::notify_track( | ||
| 307 | &Track::from_path(next_track.clone()), | ||
| 308 | fmt.as_deref(), | ||
| 309 | ); | ||
| 300 | "Playing next track\n".to_string() | 310 | "Playing next track\n".to_string() |
| 301 | } else { | 311 | } else { |
| 302 | audio.stop(); | 312 | audio.stop(); |
| @@ -311,7 +321,11 @@ fn process_command( | |||
| 311 | if let Ok(mut q) = queue.lock() { | 321 | if let Ok(mut q) = queue.lock() { |
| 312 | if let Some(prev_track) = q.previous() { | 322 | if let Some(prev_track) = q.previous() { |
| 313 | audio.play(prev_track.clone()); | 323 | audio.play(prev_track.clone()); |
| 314 | notification::notify_track(&Track::from_path(prev_track)); | 324 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 325 | notification::notify_track( | ||
| 326 | &Track::from_path(prev_track.clone()), | ||
| 327 | fmt.as_deref(), | ||
| 328 | ); | ||
| 315 | "Playing previous track\n".to_string() | 329 | "Playing previous track\n".to_string() |
| 316 | } else { | 330 | } else { |
| 317 | "No previous track\n".to_string() | 331 | "No previous track\n".to_string() |
| @@ -371,7 +385,13 @@ fn process_command( | |||
| 371 | } else { | 385 | } else { |
| 372 | ("last", rest) | 386 | ("last", rest) |
| 373 | }; | 387 | }; |
| 374 | let path_str = path_str.to_string(); | 388 | let path_str = if path_str.starts_with("~/") { |
| 389 | std::env::var("HOME") | ||
| 390 | .map(|h| path_str.replacen("~", &h, 1)) | ||
| 391 | .unwrap_or_else(|_| path_str.to_string()) | ||
| 392 | } else { | ||
| 393 | path_str.to_string() | ||
| 394 | }; | ||
| 375 | 395 | ||
| 376 | if path_str.is_empty() { | 396 | if path_str.is_empty() { |
| 377 | return "Usage: play [now|next|last] <path>\n".to_string(); | 397 | return "Usage: play [now|next|last] <path>\n".to_string(); |
| @@ -390,7 +410,9 @@ fn process_command( | |||
| 390 | } | 410 | } |
| 391 | if let Some(track) = q.track_at(q.position()) { | 411 | if let Some(track) = q.track_at(q.position()) { |
| 392 | audio.play(track.path.clone()); | 412 | audio.play(track.path.clone()); |
| 393 | notification::notify_track(track); | 413 | let fmt = |
| 414 | Config::load().ok().and_then(|c| c.display_format.clone()); | ||
| 415 | notification::notify_track(track, fmt.as_deref()); | ||
| 394 | } | 416 | } |
| 395 | format!("Playing now: {}\n", path_str) | 417 | format!("Playing now: {}\n", path_str) |
| 396 | } | 418 | } |
| @@ -405,7 +427,9 @@ fn process_command( | |||
| 405 | if was_empty && !was_playing { | 427 | if was_empty && !was_playing { |
| 406 | let track = Track::from_path(path.clone()); | 428 | let track = Track::from_path(path.clone()); |
| 407 | audio.play(path); | 429 | audio.play(path); |
| 408 | notification::notify_track(&track); | 430 | let fmt = |
| 431 | Config::load().ok().and_then(|c| c.display_format.clone()); | ||
| 432 | notification::notify_track(&track, fmt.as_deref()); | ||
| 409 | format!("Playing: {}\n", path_str) | 433 | format!("Playing: {}\n", path_str) |
| 410 | } else { | 434 | } else { |
| 411 | format!("Queued: {}\n", path_str) | 435 | format!("Queued: {}\n", path_str) |
| @@ -431,7 +455,10 @@ fn process_command( | |||
| 431 | } | 455 | } |
| 432 | if let Some(track) = q.track_at(q.position()) { | 456 | if let Some(track) = q.track_at(q.position()) { |
| 433 | audio.play(track.path.clone()); | 457 | audio.play(track.path.clone()); |
| 434 | notification::notify_track(track); | 458 | let fmt = Config::load() |
| 459 | .ok() | ||
| 460 | .and_then(|c| c.display_format.clone()); | ||
| 461 | notification::notify_track(track, fmt.as_deref()); | ||
| 435 | } | 462 | } |
| 436 | format!( | 463 | format!( |
| 437 | "Playing now: {} tracks from {}\n", | 464 | "Playing now: {} tracks from {}\n", |
| @@ -459,7 +486,10 @@ fn process_command( | |||
| 459 | && let Some(track) = q.current() | 486 | && let Some(track) = q.current() |
| 460 | { | 487 | { |
| 461 | audio.play(track.path.clone()); | 488 | audio.play(track.path.clone()); |
| 462 | notification::notify_track(track); | 489 | let fmt = Config::load() |
| 490 | .ok() | ||
| 491 | .and_then(|c| c.display_format.clone()); | ||
| 492 | notification::notify_track(track, fmt.as_deref()); | ||
| 463 | } | 493 | } |
| 464 | format!("Queued {} tracks from: {}\n", files.len(), path_str) | 494 | format!("Queued {} tracks from: {}\n", files.len(), path_str) |
| 465 | } | 495 | } |
| @@ -512,9 +542,13 @@ fn process_command( | |||
| 512 | q.shuffled_indices().to_vec() | 542 | q.shuffled_indices().to_vec() |
| 513 | }; | 543 | }; |
| 514 | let is_playing = audio.is_playing(); | 544 | let is_playing = audio.is_playing(); |
| 545 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | ||
| 515 | for (display_idx, &track_idx) in indices.iter().enumerate() { | 546 | for (display_idx, &track_idx) in indices.iter().enumerate() { |
| 516 | if let Some(track) = q.track_at(track_idx) { | 547 | if let Some(track) = q.track_at(track_idx) { |
| 517 | let track_str = track.display_string(display_idx, q.len()); | 548 | let track_str = match fmt.as_deref() { |
| 549 | Some(f) => track.format(f), | ||
| 550 | None => track.display_string(display_idx, q.len()), | ||
| 551 | }; | ||
| 518 | if track_idx == playing_track_idx && is_playing { | 552 | if track_idx == playing_track_idx && is_playing { |
| 519 | output.push_str(&format!("> {} (playing)\n", track_str)); | 553 | output.push_str(&format!("> {} (playing)\n", track_str)); |
| 520 | } else { | 554 | } else { |
diff --git a/src/notification.rs b/src/notification.rs index 2f7baa2..4947e6f 100644 --- a/src/notification.rs +++ b/src/notification.rs | |||
| @@ -1,15 +1,18 @@ | |||
| 1 | use crate::queue::Track; | 1 | use crate::queue::Track; |
| 2 | 2 | ||
| 3 | pub fn notify_track(track: &Track) { | 3 | pub fn notify_track(track: &Track, format: Option<&str>) { |
| 4 | let body = match (track.artist.as_deref(), track.title.as_deref()) { | 4 | let body = match format { |
| 5 | (Some(a), Some(t)) => format!("{} - {}", a, t), | 5 | Some(fmt) => track.format(fmt), |
| 6 | (_, Some(t)) => t.to_string(), | 6 | None => match (track.artist.as_deref(), track.title.as_deref()) { |
| 7 | _ => track | 7 | (Some(a), Some(t)) => format!("{} - {}", a, t), |
| 8 | .path | 8 | (_, Some(t)) => t.to_string(), |
| 9 | .file_name() | 9 | _ => track |
| 10 | .and_then(|n| n.to_str()) | 10 | .path |
| 11 | .unwrap_or("Unknown") | 11 | .file_name() |
| 12 | .to_string(), | 12 | .and_then(|n| n.to_str()) |
| 13 | .unwrap_or("Unknown") | ||
| 14 | .to_string(), | ||
| 15 | }, | ||
| 13 | }; | 16 | }; |
| 14 | 17 | ||
| 15 | let _ = notify_rust::Notification::new() | 18 | let _ = notify_rust::Notification::new() |
diff --git a/src/queue.rs b/src/queue.rs index 2c20356..d42203f 100644 --- a/src/queue.rs +++ b/src/queue.rs | |||
| @@ -117,6 +117,22 @@ impl Track { | |||
| 117 | format!("{:0width$}. {} - {}", index + 1, album_artist, title) | 117 | format!("{:0width$}. {} - {}", index + 1, album_artist, title) |
| 118 | } | 118 | } |
| 119 | } | 119 | } |
| 120 | |||
| 121 | pub fn format(&self, format_str: &str) -> String { | ||
| 122 | let artist = self.artist.clone().unwrap_or_default(); | ||
| 123 | let album = self.album.clone().unwrap_or_default(); | ||
| 124 | let title = self.title.clone().unwrap_or_else(|| { | ||
| 125 | self.path | ||
| 126 | .file_name() | ||
| 127 | .and_then(|n| n.to_str()) | ||
| 128 | .unwrap_or("Unknown") | ||
| 129 | .to_string() | ||
| 130 | }); | ||
| 131 | format_str | ||
| 132 | .replace("$ARTIST", &artist) | ||
| 133 | .replace("$ALBUM", &album) | ||
| 134 | .replace("$TRACK", &title) | ||
| 135 | } | ||
| 120 | } | 136 | } |
| 121 | 137 | ||
| 122 | pub struct Queue { | 138 | pub struct Queue { |
