aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/cli.rs12
-rw-r--r--src/config.rs186
-rw-r--r--src/daemon.rs66
-rw-r--r--src/notification.rs23
-rw-r--r--src/queue.rs16
5 files changed, 237 insertions, 66 deletions
diff --git a/src/cli.rs b/src/cli.rs
index bd81983..e0cea63 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -14,9 +14,9 @@ pub struct Cli {
14#[derive(Subcommand)] 14#[derive(Subcommand)]
15pub enum Commands { 15pub 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 {
101impl Cli { 101impl 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
21impl Config { 21impl 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
13pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { 13pub 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
218fn send_command_raw(cmd: String) -> Result<(), Box<dyn std::error::Error>> { 221fn 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 @@
1use crate::queue::Track; 1use crate::queue::Track;
2 2
3pub fn notify_track(track: &Track) { 3pub 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
122pub struct Queue { 138pub struct Queue {