diff options
| author | salmonik <raisersoft@proton.me> | 2026-05-08 00:55:52 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-05-08 00:55:52 +0200 |
| commit | c88f056289040533b692fdf0e7cd06d2988e5c92 (patch) | |
| tree | e18e0cfb02631b801c16d50808bc30bb85f50e4d /src | |
| parent | Extract queue module, add comprehensive tests, fix queue/play bugs (diff) | |
| download | rsplayer-c88f056289040533b692fdf0e7cd06d2988e5c92.tar.gz rsplayer-c88f056289040533b692fdf0e7cd06d2988e5c92.zip | |
Add desktop notifications for track changes, queue end, and errors
Uses notify-rust to send D-Bus desktop notifications from the daemon
when tracks change, the queue empties, or audio errors occur.
Diffstat (limited to '')
| -rw-r--r-- | src/audio.rs | 4 | ||||
| -rw-r--r-- | src/daemon.rs | 24 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/main.rs | 1 | ||||
| -rw-r--r-- | src/notification.rs | 36 |
5 files changed, 63 insertions, 3 deletions
diff --git a/src/audio.rs b/src/audio.rs index 58ad2bf..7218f84 100644 --- a/src/audio.rs +++ b/src/audio.rs | |||
| @@ -1,3 +1,4 @@ | |||
| 1 | use crate::notification; | ||
| 1 | use std::path::PathBuf; | 2 | use std::path::PathBuf; |
| 2 | use std::sync::atomic::{AtomicBool, Ordering}; | 3 | use std::sync::atomic::{AtomicBool, Ordering}; |
| 3 | use std::sync::mpsc::{self, Receiver, Sender}; | 4 | use std::sync::mpsc::{self, Receiver, Sender}; |
| @@ -331,6 +332,7 @@ fn audio_thread( | |||
| 331 | Ok(stream) => { | 332 | Ok(stream) => { |
| 332 | if let Err(e) = stream.play() { | 333 | if let Err(e) = stream.play() { |
| 333 | eprintln!("Failed to start stream: {}", e); | 334 | eprintln!("Failed to start stream: {}", e); |
| 335 | notification::notify_error("Failed to start audio stream"); | ||
| 334 | is_playing.store(false, Ordering::SeqCst); | 336 | is_playing.store(false, Ordering::SeqCst); |
| 335 | } else { | 337 | } else { |
| 336 | current_stream = Some(stream); | 338 | current_stream = Some(stream); |
| @@ -338,12 +340,14 @@ fn audio_thread( | |||
| 338 | } | 340 | } |
| 339 | Err(e) => { | 341 | Err(e) => { |
| 340 | eprintln!("Failed to create stream: {}", e); | 342 | eprintln!("Failed to create stream: {}", e); |
| 343 | notification::notify_error("Failed to create audio stream"); | ||
| 341 | is_playing.store(false, Ordering::SeqCst); | 344 | is_playing.store(false, Ordering::SeqCst); |
| 342 | } | 345 | } |
| 343 | } | 346 | } |
| 344 | } | 347 | } |
| 345 | Err(e) => { | 348 | Err(e) => { |
| 346 | eprintln!("Failed to open audio: {}", e); | 349 | eprintln!("Failed to open audio: {}", e); |
| 350 | notification::notify_error("Failed to open audio file"); | ||
| 347 | is_playing.store(false, Ordering::SeqCst); | 351 | is_playing.store(false, Ordering::SeqCst); |
| 348 | } | 352 | } |
| 349 | } | 353 | } |
diff --git a/src/daemon.rs b/src/daemon.rs index 0921e11..cca9b11 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | use crate::audio::AudioEngine; | 1 | use crate::audio::AudioEngine; |
| 2 | use crate::config::Config; | 2 | use crate::config::Config; |
| 3 | use crate::notification; | ||
| 3 | use crate::queue::{Queue, ShuffleMode, Track}; | 4 | use crate::queue::{Queue, ShuffleMode, Track}; |
| 4 | use std::io::{Read, Write}; | 5 | use std::io::{Read, Write}; |
| 5 | use std::os::unix::net::{UnixListener, UnixStream}; | 6 | use std::os::unix::net::{UnixListener, UnixStream}; |
| @@ -97,12 +98,15 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 97 | { | 98 | { |
| 98 | let original_position = q.position(); | 99 | let original_position = q.position(); |
| 99 | let mut started = false; | 100 | let mut started = false; |
| 101 | let mut had_next = false; | ||
| 100 | let max_trials = 4usize; | 102 | let max_trials = 4usize; |
| 101 | for _ in 0..max_trials { | 103 | for _ in 0..max_trials { |
| 102 | if let Some(next_track) = q.next() { | 104 | if let Some(next_track) = q.next() { |
| 105 | had_next = true; | ||
| 103 | audio_engine.play(next_track.clone()); | 106 | audio_engine.play(next_track.clone()); |
| 104 | if wait_for_start(&audio_engine, &next_track, 500) { | 107 | if wait_for_start(&audio_engine, &next_track, 500) { |
| 105 | started = true; | 108 | started = true; |
| 109 | notification::notify_track(&Track::from_path(next_track)); | ||
| 106 | break; | 110 | break; |
| 107 | } | 111 | } |
| 108 | } else { | 112 | } else { |
| @@ -112,6 +116,11 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 112 | if !started { | 116 | if !started { |
| 113 | q.set_position(original_position); | 117 | q.set_position(original_position); |
| 114 | audio_engine.stop(); | 118 | audio_engine.stop(); |
| 119 | if had_next { | ||
| 120 | notification::notify_error("Playback failed"); | ||
| 121 | } else { | ||
| 122 | notification::notify_queue_ended(); | ||
| 123 | } | ||
| 115 | } | 124 | } |
| 116 | } | 125 | } |
| 117 | 126 | ||
| @@ -286,10 +295,12 @@ fn process_command( | |||
| 286 | "next" => { | 295 | "next" => { |
| 287 | if let Ok(mut q) = queue.lock() { | 296 | if let Ok(mut q) = queue.lock() { |
| 288 | if let Some(next_track) = q.next() { | 297 | if let Some(next_track) = q.next() { |
| 289 | audio.play(next_track); | 298 | audio.play(next_track.clone()); |
| 299 | notification::notify_track(&Track::from_path(next_track)); | ||
| 290 | "Playing next track\n".to_string() | 300 | "Playing next track\n".to_string() |
| 291 | } else { | 301 | } else { |
| 292 | audio.stop(); | 302 | audio.stop(); |
| 303 | notification::notify_queue_ended(); | ||
| 293 | "Queue empty, stopped\n".to_string() | 304 | "Queue empty, stopped\n".to_string() |
| 294 | } | 305 | } |
| 295 | } else { | 306 | } else { |
| @@ -299,7 +310,8 @@ fn process_command( | |||
| 299 | "previous" => { | 310 | "previous" => { |
| 300 | if let Ok(mut q) = queue.lock() { | 311 | if let Ok(mut q) = queue.lock() { |
| 301 | if let Some(prev_track) = q.previous() { | 312 | if let Some(prev_track) = q.previous() { |
| 302 | audio.play(prev_track); | 313 | audio.play(prev_track.clone()); |
| 314 | notification::notify_track(&Track::from_path(prev_track)); | ||
| 303 | "Playing previous track\n".to_string() | 315 | "Playing previous track\n".to_string() |
| 304 | } else { | 316 | } else { |
| 305 | "No previous track\n".to_string() | 317 | "No previous track\n".to_string() |
| @@ -378,6 +390,7 @@ fn process_command( | |||
| 378 | } | 390 | } |
| 379 | if let Some(track) = q.track_at(q.position()) { | 391 | if let Some(track) = q.track_at(q.position()) { |
| 380 | audio.play(track.path.clone()); | 392 | audio.play(track.path.clone()); |
| 393 | notification::notify_track(track); | ||
| 381 | } | 394 | } |
| 382 | format!("Playing now: {}\n", path_str) | 395 | format!("Playing now: {}\n", path_str) |
| 383 | } | 396 | } |
| @@ -389,7 +402,9 @@ fn process_command( | |||
| 389 | let was_playing = audio.is_playing(); | 402 | let was_playing = audio.is_playing(); |
| 390 | q.add_track(path.clone()); | 403 | q.add_track(path.clone()); |
| 391 | if !was_playing { | 404 | if !was_playing { |
| 405 | let track = Track::from_path(path.clone()); | ||
| 392 | audio.play(path); | 406 | audio.play(path); |
| 407 | notification::notify_track(&track); | ||
| 393 | format!("Playing: {}\n", path_str) | 408 | format!("Playing: {}\n", path_str) |
| 394 | } else { | 409 | } else { |
| 395 | format!("Queued: {}\n", path_str) | 410 | format!("Queued: {}\n", path_str) |
| @@ -415,6 +430,7 @@ fn process_command( | |||
| 415 | } | 430 | } |
| 416 | if let Some(track) = q.track_at(q.position()) { | 431 | if let Some(track) = q.track_at(q.position()) { |
| 417 | audio.play(track.path.clone()); | 432 | audio.play(track.path.clone()); |
| 433 | notification::notify_track(track); | ||
| 418 | } | 434 | } |
| 419 | format!( | 435 | format!( |
| 420 | "Playing now: {} tracks from {}\n", | 436 | "Playing now: {} tracks from {}\n", |
| @@ -439,7 +455,9 @@ fn process_command( | |||
| 439 | if !audio.is_playing() | 455 | if !audio.is_playing() |
| 440 | && let Some(track) = files.first() | 456 | && let Some(track) = files.first() |
| 441 | { | 457 | { |
| 442 | audio.play(track.clone()); | 458 | let track = Track::from_path(track.clone()); |
| 459 | audio.play(track.path.clone()); | ||
| 460 | notification::notify_track(&track); | ||
| 443 | } | 461 | } |
| 444 | format!("Queued {} tracks from: {}\n", files.len(), path_str) | 462 | format!("Queued {} tracks from: {}\n", files.len(), path_str) |
| 445 | } | 463 | } |
| @@ -3,6 +3,7 @@ pub mod cli; | |||
| 3 | pub mod config; | 3 | pub mod config; |
| 4 | pub mod daemon; | 4 | pub mod daemon; |
| 5 | pub mod database; | 5 | pub mod database; |
| 6 | pub mod notification; | ||
| 6 | pub mod playback; | 7 | pub mod playback; |
| 7 | pub mod queue; | 8 | pub mod queue; |
| 8 | pub mod scrobbler; | 9 | pub mod scrobbler; |
diff --git a/src/main.rs b/src/main.rs index aac80f0..c1e42d0 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -3,6 +3,7 @@ mod cli; | |||
| 3 | mod config; | 3 | mod config; |
| 4 | mod daemon; | 4 | mod daemon; |
| 5 | mod database; | 5 | mod database; |
| 6 | mod notification; | ||
| 6 | mod playback; | 7 | mod playback; |
| 7 | mod queue; | 8 | mod queue; |
| 8 | mod scrobbler; | 9 | mod scrobbler; |
diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 0000000..2f7baa2 --- /dev/null +++ b/src/notification.rs | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | use crate::queue::Track; | ||
| 2 | |||
| 3 | pub fn notify_track(track: &Track) { | ||
| 4 | let body = match (track.artist.as_deref(), track.title.as_deref()) { | ||
| 5 | (Some(a), Some(t)) => format!("{} - {}", a, t), | ||
| 6 | (_, Some(t)) => t.to_string(), | ||
| 7 | _ => track | ||
| 8 | .path | ||
| 9 | .file_name() | ||
| 10 | .and_then(|n| n.to_str()) | ||
| 11 | .unwrap_or("Unknown") | ||
| 12 | .to_string(), | ||
| 13 | }; | ||
| 14 | |||
| 15 | let _ = notify_rust::Notification::new() | ||
| 16 | .summary("Now Playing") | ||
| 17 | .body(&body) | ||
| 18 | .appname("rsp") | ||
| 19 | .show(); | ||
| 20 | } | ||
| 21 | |||
| 22 | pub fn notify_queue_ended() { | ||
| 23 | let _ = notify_rust::Notification::new() | ||
| 24 | .summary("Playback Ended") | ||
| 25 | .body("The queue is empty") | ||
| 26 | .appname("rsp") | ||
| 27 | .show(); | ||
| 28 | } | ||
| 29 | |||
| 30 | pub fn notify_error(context: &str) { | ||
| 31 | let _ = notify_rust::Notification::new() | ||
| 32 | .summary("rsp Error") | ||
| 33 | .body(&format!("{} - Check the logs for details", context)) | ||
| 34 | .appname("rsp") | ||
| 35 | .show(); | ||
| 36 | } | ||
