From c88f056289040533b692fdf0e7cd06d2988e5c92 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 8 May 2026 00:55:52 +0200 Subject: 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. --- src/audio.rs | 4 ++++ src/daemon.rs | 24 +++++++++++++++++++++--- src/lib.rs | 1 + src/main.rs | 1 + src/notification.rs | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 src/notification.rs (limited to 'src') 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 @@ +use crate::notification; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{self, Receiver, Sender}; @@ -331,6 +332,7 @@ fn audio_thread( Ok(stream) => { if let Err(e) = stream.play() { eprintln!("Failed to start stream: {}", e); + notification::notify_error("Failed to start audio stream"); is_playing.store(false, Ordering::SeqCst); } else { current_stream = Some(stream); @@ -338,12 +340,14 @@ fn audio_thread( } Err(e) => { eprintln!("Failed to create stream: {}", e); + notification::notify_error("Failed to create audio stream"); is_playing.store(false, Ordering::SeqCst); } } } Err(e) => { eprintln!("Failed to open audio: {}", e); + notification::notify_error("Failed to open audio file"); is_playing.store(false, Ordering::SeqCst); } } 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 @@ use crate::audio::AudioEngine; use crate::config::Config; +use crate::notification; use crate::queue::{Queue, ShuffleMode, Track}; use std::io::{Read, Write}; use std::os::unix::net::{UnixListener, UnixStream}; @@ -97,12 +98,15 @@ pub fn start_daemon() -> Result<(), Box> { { let original_position = q.position(); let mut started = false; + let mut had_next = false; let max_trials = 4usize; for _ in 0..max_trials { if let Some(next_track) = q.next() { + had_next = true; audio_engine.play(next_track.clone()); if wait_for_start(&audio_engine, &next_track, 500) { started = true; + notification::notify_track(&Track::from_path(next_track)); break; } } else { @@ -112,6 +116,11 @@ pub fn start_daemon() -> Result<(), Box> { if !started { q.set_position(original_position); audio_engine.stop(); + if had_next { + notification::notify_error("Playback failed"); + } else { + notification::notify_queue_ended(); + } } } @@ -286,10 +295,12 @@ fn process_command( "next" => { if let Ok(mut q) = queue.lock() { if let Some(next_track) = q.next() { - audio.play(next_track); + audio.play(next_track.clone()); + notification::notify_track(&Track::from_path(next_track)); "Playing next track\n".to_string() } else { audio.stop(); + notification::notify_queue_ended(); "Queue empty, stopped\n".to_string() } } else { @@ -299,7 +310,8 @@ fn process_command( "previous" => { if let Ok(mut q) = queue.lock() { if let Some(prev_track) = q.previous() { - audio.play(prev_track); + audio.play(prev_track.clone()); + notification::notify_track(&Track::from_path(prev_track)); "Playing previous track\n".to_string() } else { "No previous track\n".to_string() @@ -378,6 +390,7 @@ fn process_command( } if let Some(track) = q.track_at(q.position()) { audio.play(track.path.clone()); + notification::notify_track(track); } format!("Playing now: {}\n", path_str) } @@ -389,7 +402,9 @@ fn process_command( let was_playing = audio.is_playing(); q.add_track(path.clone()); if !was_playing { + let track = Track::from_path(path.clone()); audio.play(path); + notification::notify_track(&track); format!("Playing: {}\n", path_str) } else { format!("Queued: {}\n", path_str) @@ -415,6 +430,7 @@ fn process_command( } if let Some(track) = q.track_at(q.position()) { audio.play(track.path.clone()); + notification::notify_track(track); } format!( "Playing now: {} tracks from {}\n", @@ -439,7 +455,9 @@ fn process_command( if !audio.is_playing() && let Some(track) = files.first() { - audio.play(track.clone()); + let track = Track::from_path(track.clone()); + audio.play(track.path.clone()); + notification::notify_track(&track); } format!("Queued {} tracks from: {}\n", files.len(), path_str) } diff --git a/src/lib.rs b/src/lib.rs index cc5d7bf..825872f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod cli; pub mod config; pub mod daemon; pub mod database; +pub mod notification; pub mod playback; pub mod queue; 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; mod config; mod daemon; mod database; +mod notification; mod playback; mod queue; 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 @@ +use crate::queue::Track; + +pub fn notify_track(track: &Track) { + let body = match (track.artist.as_deref(), track.title.as_deref()) { + (Some(a), Some(t)) => format!("{} - {}", a, t), + (_, Some(t)) => t.to_string(), + _ => track + .path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("Unknown") + .to_string(), + }; + + let _ = notify_rust::Notification::new() + .summary("Now Playing") + .body(&body) + .appname("rsp") + .show(); +} + +pub fn notify_queue_ended() { + let _ = notify_rust::Notification::new() + .summary("Playback Ended") + .body("The queue is empty") + .appname("rsp") + .show(); +} + +pub fn notify_error(context: &str) { + let _ = notify_rust::Notification::new() + .summary("rsp Error") + .body(&format!("{} - Check the logs for details", context)) + .appname("rsp") + .show(); +} -- cgit v1.2.3