From da0251d55d3902c90967b36b7a5441404bdf24ef Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 30 Jul 2026 18:37:52 +0200 Subject: Fix synchronous HTTP calls --- src/daemon.rs | 194 ++++++++++++++++++++++++++++++------------------------- src/mpris.rs | 22 +++---- src/scrobbler.rs | 7 +- 3 files changed, 121 insertions(+), 102 deletions(-) (limited to 'src') diff --git a/src/daemon.rs b/src/daemon.rs index 1c918a8..979c6b4 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -102,57 +102,86 @@ impl ScrobbleState { } } -pub fn try_scrobble_current(scrobbler: &RwLock, state: &mut ScrobbleState) { - if let Some(ref track) = state.track - && track.is_stream - { - crate::debug_eprintln(format_args!("scrobble: skipping scrobble for stream track")); - state.clear(); +/// Fire-and-forget version of `send_now_playing`. +/// Spawns the scrobble HTTP call on a background thread so the caller +/// (e.g. a Unix socket handler) is never blocked by network latency. +pub fn spawn_send_now_playing(scrobbler: &Arc>, track: &Track) { + if track.is_stream { return; } - if state.should_scrobble() - && let (Some(track), Some(ts)) = (&state.track, state.timestamp) - { - let artist = track.artist.as_deref().unwrap_or("(unknown artist)"); - let title = track.title.as_deref().unwrap_or("(unknown title)"); - let album = track.album.as_deref(); + let s = Arc::clone(scrobbler); + let artist = track + .artist + .clone() + .unwrap_or_else(|| "(unknown artist)".into()); + let title = track + .title + .clone() + .unwrap_or_else(|| "(unknown title)".into()); + let album = track.album.clone(); + thread::spawn(move || { crate::debug_eprintln(format_args!( - "scrobble: sending scrobble for '{} - {}' at ts={}", - artist, title, ts + "scrobble: sending now_playing for '{} - {}'", + artist, title )); - if let Ok(s) = scrobbler.read() { - s.scrobble(artist, title, album, ts); + if let Ok(s) = s.read() { + s.now_playing(&artist, &title, album.as_deref()); } - } else { - let title = state - .track - .as_ref() - .and_then(|t| t.title.as_deref()) - .unwrap_or("(no track)"); - crate::debug_eprintln(format_args!( - "scrobble: skipping scrobble for '{}' — not enough play time", - title - )); - } - state.clear(); + }); } -pub fn send_now_playing(scrobbler: &RwLock, track: &Track) { - if track.is_stream { - crate::debug_eprintln(format_args!( - "scrobble: skipping now_playing for stream track" - )); - return; - } - let artist = track.artist.as_deref().unwrap_or("(unknown artist)"); - let title = track.title.as_deref().unwrap_or("(unknown title)"); - let album = track.album.as_deref(); - crate::debug_eprintln(format_args!( - "scrobble: sending now_playing for '{} - {}'", - artist, title - )); - if let Ok(s) = scrobbler.read() { - s.now_playing(artist, title, album); +/// Fire-and-forget version of `try_scrobble_current`. +/// Extracts the scrobble decision and track data under the state lock, +/// then spawns the HTTP call on a background thread. +pub fn spawn_try_scrobble_current( + scrobbler: &Arc>, + state: &Arc>, +) { + let snapshot = { + let mut s = match state.lock() { + Ok(g) => g, + Err(_) => return, + }; + if let Some(ref track) = s.track + && track.is_stream + { + crate::debug_eprintln(format_args!("scrobble: skipping scrobble for stream track")); + s.clear(); + return; + } + if s.should_scrobble() { + let track = s.track.take(); + let ts = s.timestamp.take(); + s.start_time = None; + (track, ts) + } else { + let title = s + .track + .as_ref() + .and_then(|t| t.title.as_deref()) + .unwrap_or("(no track)"); + crate::debug_eprintln(format_args!( + "scrobble: skipping scrobble for '{}' — not enough play time", + title + )); + s.clear(); + (None, None) + } + }; + if let (Some(track), Some(ts)) = snapshot { + let s = Arc::clone(scrobbler); + thread::spawn(move || { + let artist = track.artist.as_deref().unwrap_or("(unknown artist)"); + let title = track.title.as_deref().unwrap_or("(unknown title)"); + let album = track.album.as_deref(); + crate::debug_eprintln(format_args!( + "scrobble: sending scrobble for '{} - {}' at ts={}", + artist, title, ts + )); + if let Ok(s) = s.read() { + s.scrobble(artist, title, album, ts); + } + }); } } @@ -246,6 +275,15 @@ pub fn start_daemon() -> Result<(), Box> { // Save full config config.save()?; + // Warm up D-Bus session connection so the first notification + // doesn't pay the full connection-establishment cost. + if let Err(e) = zbus::blocking::Connection::session() { + crate::debug_eprintln(format_args!( + "daemon: failed to connect to D-Bus session bus: {}", + e + )); + } + // Start MPRIS D-Bus server for external control (playerctl, etc.) crate::mpris::start_mpris_server( Arc::clone(&audio_engine), @@ -282,10 +320,8 @@ pub fn start_daemon() -> Result<(), Box> { if audio_engine.poll_track_ended() && let Ok(mut q) = queue.lock() { - // Scrobble the track that just ended - if let Ok(mut s) = scrobble_state.lock() { - try_scrobble_current(&scrobbler, &mut s); - } + // Scrobble the track that just ended (fire-and-forget) + spawn_try_scrobble_current(&scrobbler, &scrobble_state); let original_position = q.position(); let mut started = false; let mut had_next = false; @@ -297,8 +333,8 @@ pub fn start_daemon() -> Result<(), Box> { if wait_for_start(&audio_engine, &next_track, 500) { started = true; let track = Track::from_path(next_track.clone()); - // Send now playing and record scrobble start - send_now_playing(&scrobbler, &track); + // Send now playing and record scrobble start (fire-and-forget) + spawn_send_now_playing(&scrobbler, &track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -578,7 +614,7 @@ fn process_command( audio: &AudioEngine, shutdown_flag: &Arc, queue: &Arc>, - scrobbler: &RwLock, + scrobbler: &Arc>, scrobble_state: &Arc>, ) -> String { match cmd { @@ -621,9 +657,7 @@ fn process_command( } } "stop" => { - if let Ok(mut s) = scrobble_state.lock() { - try_scrobble_current(scrobbler, &mut s); - } + spawn_try_scrobble_current(scrobbler, scrobble_state); audio.stop(); if let Ok(mut q) = queue.lock() { q.clear(); @@ -670,14 +704,12 @@ fn process_command( } } "next" => { - if let Ok(mut s) = scrobble_state.lock() { - try_scrobble_current(scrobbler, &mut s); - } + spawn_try_scrobble_current(scrobbler, scrobble_state); if let Ok(mut q) = queue.lock() { if let Some(next_track) = q.next() { audio.play(next_track.clone()); let track = Track::from_path(next_track.clone()); - send_now_playing(scrobbler, &track); + spawn_send_now_playing(scrobbler, &track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -695,14 +727,12 @@ fn process_command( } } "previous" => { - if let Ok(mut s) = scrobble_state.lock() { - try_scrobble_current(scrobbler, &mut s); - } + spawn_try_scrobble_current(scrobbler, scrobble_state); if let Ok(mut q) = queue.lock() { if let Some(prev_track) = q.previous() { audio.play(prev_track.clone()); let track = Track::from_path(prev_track.clone()); - send_now_playing(scrobbler, &track); + spawn_send_now_playing(scrobbler, &track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -878,9 +908,7 @@ fn process_command( let was_empty = q.is_empty(); match position { "now" => { - if let Ok(mut s) = scrobble_state.lock() { - try_scrobble_current(scrobbler, &mut s); - } + spawn_try_scrobble_current(scrobbler, scrobble_state); // album_tracks[0] is the track to start at; insert the // whole block in correct order. if was_empty { @@ -896,7 +924,7 @@ fn process_command( } if let Some(track) = q.track_at(q.position()) { audio.play(track.path.clone()); - send_now_playing(scrobbler, track); + spawn_send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -934,7 +962,7 @@ fn process_command( && let Some(track) = q.current() { audio.play(track.path.clone()); - send_now_playing(scrobbler, track); + spawn_send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -999,9 +1027,7 @@ fn process_command( let track = Track::from_url(&path_str); match position { "now" => { - if let Ok(mut s) = scrobble_state.lock() { - try_scrobble_current(scrobbler, &mut s); - } + spawn_try_scrobble_current(scrobbler, scrobble_state); q.push_track(track.clone()); if !was_empty { let new_pos = q.position() + 1; @@ -1009,7 +1035,7 @@ fn process_command( } if let Some(track) = q.track_at(q.position()) { audio.play(track.path.clone()); - send_now_playing(scrobbler, track); + spawn_send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -1031,7 +1057,7 @@ fn process_command( && let Some(track) = q.current() { audio.play(track.path.clone()); - send_now_playing(scrobbler, track); + spawn_send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -1052,9 +1078,7 @@ fn process_command( let was_empty = q.is_empty(); match position { "now" => { - if let Ok(mut s) = scrobble_state.lock() { - try_scrobble_current(scrobbler, &mut s); - } + spawn_try_scrobble_current(scrobbler, scrobble_state); q.insert_after_current(path.clone()); if !was_empty { let new_pos = q.position() + 1; @@ -1062,7 +1086,7 @@ fn process_command( } if let Some(track) = q.track_at(q.position()) { audio.play(track.path.clone()); - send_now_playing(scrobbler, track); + spawn_send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -1083,7 +1107,7 @@ fn process_command( if was_empty && !was_playing { let track = Track::from_path(path.clone()); audio.play(path); - send_now_playing(scrobbler, &track); + spawn_send_now_playing(scrobbler, &track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -1106,9 +1130,7 @@ fn process_command( let was_empty = q.is_empty(); match position { "now" => { - if let Ok(mut s) = scrobble_state.lock() { - try_scrobble_current(scrobbler, &mut s); - } + spawn_try_scrobble_current(scrobbler, scrobble_state); for file in files.iter().rev() { q.insert_after_current(file.clone()); } @@ -1118,7 +1140,7 @@ fn process_command( } if let Some(track) = q.track_at(q.position()) { audio.play(track.path.clone()); - send_now_playing(scrobbler, track); + spawn_send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -1152,7 +1174,7 @@ fn process_command( q.shuffle_initial(); if let Some(track) = q.current() { audio.play(track.path.clone()); - send_now_playing(scrobbler, track); + spawn_send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -1206,9 +1228,7 @@ fn process_command( let was_empty = q.is_empty(); match position { "now" => { - if let Ok(mut s) = scrobble_state.lock() { - try_scrobble_current(scrobbler, &mut s); - } + spawn_try_scrobble_current(scrobbler, scrobble_state); for t in tracks.iter().rev() { q.insert_after_current(t.path.clone()); } @@ -1218,7 +1238,7 @@ fn process_command( } if let Some(track) = q.track_at(q.position()) { audio.play(track.path.clone()); - send_now_playing(scrobbler, track); + spawn_send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } @@ -1262,7 +1282,7 @@ fn process_command( } if let Some(track) = q.current() { audio.play(track.path.clone()); - send_now_playing(scrobbler, track); + spawn_send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } diff --git a/src/mpris.rs b/src/mpris.rs index a7ac360..39630b0 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -162,14 +162,12 @@ impl MprisPlayer { fn next(&self) { crate::debug_eprintln(format_args!("mpris: next")); - if let Ok(mut s) = self.scrobble_state.lock() { - crate::daemon::try_scrobble_current(&self.scrobbler, &mut s); - } + crate::daemon::spawn_try_scrobble_current(&self.scrobbler, &self.scrobble_state); if let Ok(mut q) = self.queue.lock() { if let Some(next_path) = q.next() { self.audio.play(next_path.clone()); let track = Track::from_path(next_path); - crate::daemon::send_now_playing(&self.scrobbler, &track); + crate::daemon::spawn_send_now_playing(&self.scrobbler, &track); if let Ok(mut s) = self.scrobble_state.lock() { s.start(track.clone()); } @@ -185,15 +183,13 @@ impl MprisPlayer { fn previous(&self) { crate::debug_eprintln(format_args!("mpris: previous")); - if let Ok(mut s) = self.scrobble_state.lock() { - crate::daemon::try_scrobble_current(&self.scrobbler, &mut s); - } + crate::daemon::spawn_try_scrobble_current(&self.scrobbler, &self.scrobble_state); if let Ok(mut q) = self.queue.lock() && let Some(prev_path) = q.previous() { self.audio.play(prev_path.clone()); let track = Track::from_path(prev_path); - crate::daemon::send_now_playing(&self.scrobbler, &track); + crate::daemon::spawn_send_now_playing(&self.scrobbler, &track); if let Ok(mut s) = self.scrobble_state.lock() { s.start(track.clone()); } @@ -221,9 +217,7 @@ impl MprisPlayer { fn stop(&self) { crate::debug_eprintln(format_args!("mpris: stop")); - if let Ok(mut s) = self.scrobble_state.lock() { - crate::daemon::try_scrobble_current(&self.scrobbler, &mut s); - } + crate::daemon::spawn_try_scrobble_current(&self.scrobbler, &self.scrobble_state); self.audio.stop(); if let Ok(mut q) = self.queue.lock() { q.clear(); @@ -240,7 +234,7 @@ impl MprisPlayer { && let Some(track) = q.current() { self.audio.play(track.path.clone()); - crate::daemon::send_now_playing(&self.scrobbler, track); + crate::daemon::spawn_send_now_playing(&self.scrobbler, track); if let Ok(mut s) = self.scrobble_state.lock() { s.start(track.clone()); } @@ -277,7 +271,7 @@ impl MprisPlayer { && let Some(t) = q.current() { self.audio.play(t.path.clone()); - crate::daemon::send_now_playing(&self.scrobbler, t); + crate::daemon::spawn_send_now_playing(&self.scrobbler, t); if let Ok(mut s) = self.scrobble_state.lock() { s.start(t.clone()); } @@ -289,7 +283,7 @@ impl MprisPlayer { if was_empty && !was_playing { let track = Track::from_path(path); self.audio.play(track.path.clone()); - crate::daemon::send_now_playing(&self.scrobbler, &track); + crate::daemon::spawn_send_now_playing(&self.scrobbler, &track); if let Ok(mut s) = self.scrobble_state.lock() { s.start(track.clone()); } diff --git a/src/scrobbler.rs b/src/scrobbler.rs index 40d3ce6..e6e8a73 100644 --- a/src/scrobbler.rs +++ b/src/scrobbler.rs @@ -8,6 +8,7 @@ use md5::{Digest, Md5}; use reqwest::blocking::Client; use std::collections::BTreeMap; +use std::time::Duration; pub struct Scrobbler { pub lastfm_api_key: Option, @@ -33,7 +34,11 @@ impl Scrobbler { lastfm_api_secret, lastfm_session_key, listenbrainz_token, - client: Client::new(), + client: Client::builder() + .connect_timeout(Duration::from_secs(10)) + .timeout(Duration::from_secs(30)) + .build() + .expect("valid reqwest Client"), } } -- cgit v1.2.3