diff options
| author | salmonik <raisersoft@proton.me> | 2026-05-16 11:43:32 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-05-16 11:43:32 +0200 |
| commit | 04add15691578316ff3da5b5a07d05b314488370 (patch) | |
| tree | 458b1066001697faa75466d51c7dfd9008ccac16 /src/daemon.rs | |
| parent | aesthetic changes (diff) | |
| download | rsplayer-04add15691578316ff3da5b5a07d05b314488370.tar.gz rsplayer-04add15691578316ff3da5b5a07d05b314488370.zip | |
Add scrobbling support (Last.fm + ListenBrainz) and rsp settings reload
- Full Last.fm API implementation with MD5 signing (track.updateNowPlaying, track.scrobble)
- Full ListenBrainz API implementation (/1/submit-listens for playing_now and single)
- ScrobbleState in daemon tracking track, start time, and unix timestamp per play
- Scrobble threshold: ≥30s play AND (≥half duration OR ≥4 min)
- Duration extraction from symphonia codec params into AudioMetadata and Track
- Token validation at daemon startup and on config change
- ListenBrainz token valid (user: Salmonium) command to re-read config from disk (avoids token in shell history)
- Desktop notifications for token validation results
- Empty-credential filtering (empty string = no-op)
Diffstat (limited to '')
| -rw-r--r-- | src/daemon.rs | 325 |
1 files changed, 311 insertions, 14 deletions
diff --git a/src/daemon.rs b/src/daemon.rs index 2b38025..06b925e 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -2,13 +2,192 @@ use crate::audio::AudioEngine; | |||
| 2 | use crate::config::Config; | 2 | use crate::config::Config; |
| 3 | use crate::notification; | 3 | use crate::notification; |
| 4 | use crate::queue::{Queue, ShuffleMode, Track}; | 4 | use crate::queue::{Queue, ShuffleMode, Track}; |
| 5 | use crate::scrobbler::Scrobbler; | ||
| 5 | use std::io::{Read, Write}; | 6 | use std::io::{Read, Write}; |
| 6 | use std::os::unix::net::{UnixListener, UnixStream}; | 7 | use std::os::unix::net::{UnixListener, UnixStream}; |
| 7 | use std::path::{Path, PathBuf}; | 8 | use std::path::{Path, PathBuf}; |
| 8 | use std::sync::atomic::{AtomicBool, Ordering}; | 9 | use std::sync::atomic::{AtomicBool, Ordering}; |
| 9 | use std::sync::{Arc, Mutex}; | 10 | use std::sync::{Arc, Mutex, RwLock}; |
| 10 | use std::thread; | 11 | use std::thread; |
| 11 | use std::time::Duration; | 12 | use std::time::{Duration, Instant, SystemTime}; |
| 13 | |||
| 14 | struct ScrobbleState { | ||
| 15 | track: Option<Track>, | ||
| 16 | start_time: Option<Instant>, | ||
| 17 | timestamp: Option<u64>, | ||
| 18 | } | ||
| 19 | |||
| 20 | impl ScrobbleState { | ||
| 21 | fn new() -> Self { | ||
| 22 | ScrobbleState { | ||
| 23 | track: None, | ||
| 24 | start_time: None, | ||
| 25 | timestamp: None, | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | fn start(&mut self, track: Track) { | ||
| 30 | let ts = SystemTime::now() | ||
| 31 | .duration_since(SystemTime::UNIX_EPOCH) | ||
| 32 | .unwrap_or_default() | ||
| 33 | .as_secs(); | ||
| 34 | let title = track.title.clone().unwrap_or_default(); | ||
| 35 | let artist = track.artist.clone().unwrap_or_default(); | ||
| 36 | eprintln!( | ||
| 37 | "scrobble: tracking start for '{} - {}' (ts={})", | ||
| 38 | artist, title, ts | ||
| 39 | ); | ||
| 40 | self.track = Some(track); | ||
| 41 | self.start_time = Some(Instant::now()); | ||
| 42 | self.timestamp = Some(ts); | ||
| 43 | } | ||
| 44 | |||
| 45 | fn should_scrobble(&self) -> bool { | ||
| 46 | let (Some(track), Some(start)) = (&self.track, self.start_time) else { | ||
| 47 | eprintln!("scrobble: should_scrobble false — no track or start_time"); | ||
| 48 | return false; | ||
| 49 | }; | ||
| 50 | let elapsed = start.elapsed().as_secs(); | ||
| 51 | let result = Self::should_scrobble_with_params(elapsed, track.duration_secs); | ||
| 52 | let threshold = track | ||
| 53 | .duration_secs | ||
| 54 | .map(|d| (d / 2.0) as u64) | ||
| 55 | .unwrap_or(u64::MAX) | ||
| 56 | .min(240); | ||
| 57 | eprintln!( | ||
| 58 | "scrobble: should_scrobble {} (elapsed={}s threshold={}s duration={:?})", | ||
| 59 | result, elapsed, threshold, track.duration_secs | ||
| 60 | ); | ||
| 61 | result | ||
| 62 | } | ||
| 63 | |||
| 64 | fn should_scrobble_with_params(elapsed: u64, duration_secs: Option<f64>) -> bool { | ||
| 65 | if elapsed < 30 { | ||
| 66 | return false; | ||
| 67 | } | ||
| 68 | let threshold = duration_secs | ||
| 69 | .map(|d| (d / 2.0) as u64) | ||
| 70 | .unwrap_or(u64::MAX) | ||
| 71 | .min(240); | ||
| 72 | elapsed >= threshold | ||
| 73 | } | ||
| 74 | |||
| 75 | fn clear(&mut self) { | ||
| 76 | self.track = None; | ||
| 77 | self.start_time = None; | ||
| 78 | self.timestamp = None; | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | #[cfg(test)] | ||
| 83 | mod tests { | ||
| 84 | use super::*; | ||
| 85 | |||
| 86 | #[test] | ||
| 87 | fn test_scrobble_state_new() { | ||
| 88 | let s = ScrobbleState::new(); | ||
| 89 | assert!(s.track.is_none()); | ||
| 90 | assert!(s.start_time.is_none()); | ||
| 91 | assert!(s.timestamp.is_none()); | ||
| 92 | } | ||
| 93 | |||
| 94 | #[test] | ||
| 95 | fn test_scrobble_state_clear() { | ||
| 96 | let mut s = ScrobbleState::new(); | ||
| 97 | let t = Track { | ||
| 98 | path: PathBuf::from("/test/song.mp3"), | ||
| 99 | title: Some("Test".into()), | ||
| 100 | artist: Some("Artist".into()), | ||
| 101 | album: None, | ||
| 102 | duration_secs: Some(200.0), | ||
| 103 | }; | ||
| 104 | s.start(t); | ||
| 105 | assert!(s.track.is_some()); | ||
| 106 | s.clear(); | ||
| 107 | assert!(s.track.is_none()); | ||
| 108 | assert!(s.start_time.is_none()); | ||
| 109 | assert!(s.timestamp.is_none()); | ||
| 110 | } | ||
| 111 | |||
| 112 | #[test] | ||
| 113 | fn test_should_scrobble_without_track_returns_false() { | ||
| 114 | assert!(!ScrobbleState::new().should_scrobble()); | ||
| 115 | } | ||
| 116 | |||
| 117 | #[test] | ||
| 118 | fn test_should_scrobble_threshold_below_30_seconds() { | ||
| 119 | assert!(!ScrobbleState::should_scrobble_with_params(29, Some(200.0))); | ||
| 120 | } | ||
| 121 | |||
| 122 | #[test] | ||
| 123 | fn test_should_scrobble_threshold_half_duration() { | ||
| 124 | assert!(ScrobbleState::should_scrobble_with_params(100, Some(200.0))); | ||
| 125 | } | ||
| 126 | |||
| 127 | #[test] | ||
| 128 | fn test_should_scrobble_threshold_below_half_duration() { | ||
| 129 | assert!(!ScrobbleState::should_scrobble_with_params(80, Some(200.0))); | ||
| 130 | } | ||
| 131 | |||
| 132 | #[test] | ||
| 133 | fn test_should_scrobble_threshold_capped_at_4min() { | ||
| 134 | assert!(ScrobbleState::should_scrobble_with_params(240, Some(600.0))); | ||
| 135 | assert!(!ScrobbleState::should_scrobble_with_params( | ||
| 136 | 239, | ||
| 137 | Some(600.0) | ||
| 138 | )); | ||
| 139 | } | ||
| 140 | |||
| 141 | #[test] | ||
| 142 | fn test_should_scrobble_no_duration_requires_4min() { | ||
| 143 | assert!(ScrobbleState::should_scrobble_with_params(240, None)); | ||
| 144 | assert!(!ScrobbleState::should_scrobble_with_params(239, None)); | ||
| 145 | } | ||
| 146 | |||
| 147 | #[test] | ||
| 148 | fn test_should_scrobble_short_song() { | ||
| 149 | assert!(ScrobbleState::should_scrobble_with_params(30, Some(60.0))); | ||
| 150 | assert!(!ScrobbleState::should_scrobble_with_params(15, Some(60.0))); | ||
| 151 | } | ||
| 152 | } | ||
| 153 | |||
| 154 | fn try_scrobble_current(scrobbler: &RwLock<Scrobbler>, state: &mut ScrobbleState) { | ||
| 155 | if state.should_scrobble() | ||
| 156 | && let (Some(track), Some(ts)) = (&state.track, state.timestamp) | ||
| 157 | { | ||
| 158 | let artist = track.artist.as_deref().unwrap_or("(unknown artist)"); | ||
| 159 | let title = track.title.as_deref().unwrap_or("(unknown title)"); | ||
| 160 | let album = track.album.as_deref(); | ||
| 161 | eprintln!( | ||
| 162 | "scrobble: sending scrobble for '{} - {}' at ts={}", | ||
| 163 | artist, title, ts | ||
| 164 | ); | ||
| 165 | if let Ok(s) = scrobbler.read() { | ||
| 166 | s.scrobble(artist, title, album, ts); | ||
| 167 | } | ||
| 168 | } else { | ||
| 169 | let title = state | ||
| 170 | .track | ||
| 171 | .as_ref() | ||
| 172 | .and_then(|t| t.title.as_deref()) | ||
| 173 | .unwrap_or("(no track)"); | ||
| 174 | eprintln!( | ||
| 175 | "scrobble: skipping scrobble for '{}' — not enough play time", | ||
| 176 | title | ||
| 177 | ); | ||
| 178 | } | ||
| 179 | state.clear(); | ||
| 180 | } | ||
| 181 | |||
| 182 | fn send_now_playing(scrobbler: &RwLock<Scrobbler>, track: &Track) { | ||
| 183 | let artist = track.artist.as_deref().unwrap_or("(unknown artist)"); | ||
| 184 | let title = track.title.as_deref().unwrap_or("(unknown title)"); | ||
| 185 | let album = track.album.as_deref(); | ||
| 186 | eprintln!("scrobble: sending now_playing for '{} - {}'", artist, title); | ||
| 187 | if let Ok(s) = scrobbler.read() { | ||
| 188 | s.now_playing(artist, title, album); | ||
| 189 | } | ||
| 190 | } | ||
| 12 | 191 | ||
| 13 | pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | 192 | pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { |
| 14 | let config = Config::load()?; | 193 | let config = Config::load()?; |
| @@ -68,6 +247,26 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 68 | } | 247 | } |
| 69 | } | 248 | } |
| 70 | 249 | ||
| 250 | // Create scrobbler from config | ||
| 251 | let scrobbler = Arc::new(RwLock::new(Scrobbler::new( | ||
| 252 | config.lastfm_api_key.clone(), | ||
| 253 | config.lastfm_api_secret.clone(), | ||
| 254 | config.lastfm_session_key.clone(), | ||
| 255 | config.listenbrainz_token.clone(), | ||
| 256 | ))); | ||
| 257 | // Validate ListenBrainz token at startup | ||
| 258 | if let Some(token) = &config.listenbrainz_token | ||
| 259 | && !token.is_empty() | ||
| 260 | && let Ok(s) = scrobbler.read() | ||
| 261 | { | ||
| 262 | match s.validate_listenbrainz_token() { | ||
| 263 | Ok(Some(user)) => eprintln!("scrobble: listenbrainz token valid (user: {})", user), | ||
| 264 | Ok(None) => eprintln!("scrobble: listenbrainz token invalid, scrobbling disabled"), | ||
| 265 | Err(e) => eprintln!("scrobble: listenbrainz validation error: {}", e), | ||
| 266 | } | ||
| 267 | } | ||
| 268 | let scrobble_state = Arc::new(Mutex::new(ScrobbleState::new())); | ||
| 269 | |||
| 71 | // Save full config | 270 | // Save full config |
| 72 | config.save()?; | 271 | config.save()?; |
| 73 | 272 | ||
| @@ -98,6 +297,10 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 98 | if audio_engine.poll_track_ended() | 297 | if audio_engine.poll_track_ended() |
| 99 | && let Ok(mut q) = queue.lock() | 298 | && let Ok(mut q) = queue.lock() |
| 100 | { | 299 | { |
| 300 | // Scrobble the track that just ended | ||
| 301 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 302 | try_scrobble_current(&scrobbler, &mut s); | ||
| 303 | } | ||
| 101 | let original_position = q.position(); | 304 | let original_position = q.position(); |
| 102 | let mut started = false; | 305 | let mut started = false; |
| 103 | let mut had_next = false; | 306 | let mut had_next = false; |
| @@ -108,8 +311,14 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 108 | audio_engine.play(next_track.clone()); | 311 | audio_engine.play(next_track.clone()); |
| 109 | if wait_for_start(&audio_engine, &next_track, 500) { | 312 | if wait_for_start(&audio_engine, &next_track, 500) { |
| 110 | started = true; | 313 | started = true; |
| 314 | let track = Track::from_path(next_track.clone()); | ||
| 315 | // Send now playing and record scrobble start | ||
| 316 | send_now_playing(&scrobbler, &track); | ||
| 317 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 318 | s.start(track.clone()); | ||
| 319 | } | ||
| 111 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 320 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 112 | notification::notify_track(&Track::from_path(next_track), fmt.as_deref()); | 321 | notification::notify_track(&track, fmt.as_deref()); |
| 113 | break; | 322 | break; |
| 114 | } | 323 | } |
| 115 | } else { | 324 | } else { |
| @@ -132,8 +341,17 @@ pub fn start_daemon() -> Result<(), Box<dyn std::error::Error>> { | |||
| 132 | let audio_clone = Arc::clone(&audio_engine); | 341 | let audio_clone = Arc::clone(&audio_engine); |
| 133 | let shutdown_flag_clone = Arc::clone(&shutdown_flag); | 342 | let shutdown_flag_clone = Arc::clone(&shutdown_flag); |
| 134 | let queue_clone = Arc::clone(&queue); | 343 | let queue_clone = Arc::clone(&queue); |
| 344 | let scrobbler_clone = Arc::clone(&scrobbler); | ||
| 345 | let scrobble_state_clone = Arc::clone(&scrobble_state); | ||
| 135 | thread::spawn(move || { | 346 | thread::spawn(move || { |
| 136 | handle_client(stream, audio_clone, shutdown_flag_clone, queue_clone); | 347 | handle_client( |
| 348 | stream, | ||
| 349 | audio_clone, | ||
| 350 | shutdown_flag_clone, | ||
| 351 | queue_clone, | ||
| 352 | scrobbler_clone, | ||
| 353 | scrobble_state_clone, | ||
| 354 | ); | ||
| 137 | }); | 355 | }); |
| 138 | } | 356 | } |
| 139 | Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { | 357 | Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { |
| @@ -236,11 +454,20 @@ fn handle_client( | |||
| 236 | audio: Arc<AudioEngine>, | 454 | audio: Arc<AudioEngine>, |
| 237 | shutdown_flag: Arc<AtomicBool>, | 455 | shutdown_flag: Arc<AtomicBool>, |
| 238 | queue: Arc<Mutex<Queue>>, | 456 | queue: Arc<Mutex<Queue>>, |
| 457 | scrobbler: Arc<RwLock<Scrobbler>>, | ||
| 458 | scrobble_state: Arc<Mutex<ScrobbleState>>, | ||
| 239 | ) { | 459 | ) { |
| 240 | let mut buffer = [0; 1024]; | 460 | let mut buffer = [0; 1024]; |
| 241 | if let Ok(size) = stream.read(&mut buffer) { | 461 | if let Ok(size) = stream.read(&mut buffer) { |
| 242 | let cmd = String::from_utf8_lossy(&buffer[..size]); | 462 | let cmd = String::from_utf8_lossy(&buffer[..size]); |
| 243 | let response = process_command(cmd.trim(), &audio, &shutdown_flag, &queue); | 463 | let response = process_command( |
| 464 | cmd.trim(), | ||
| 465 | &audio, | ||
| 466 | &shutdown_flag, | ||
| 467 | &queue, | ||
| 468 | &scrobbler, | ||
| 469 | &scrobble_state, | ||
| 470 | ); | ||
| 244 | let _ = stream.write_all(response.as_bytes()); | 471 | let _ = stream.write_all(response.as_bytes()); |
| 245 | } | 472 | } |
| 246 | } | 473 | } |
| @@ -250,6 +477,8 @@ fn process_command( | |||
| 250 | audio: &AudioEngine, | 477 | audio: &AudioEngine, |
| 251 | shutdown_flag: &Arc<AtomicBool>, | 478 | shutdown_flag: &Arc<AtomicBool>, |
| 252 | queue: &Arc<Mutex<Queue>>, | 479 | queue: &Arc<Mutex<Queue>>, |
| 480 | scrobbler: &RwLock<Scrobbler>, | ||
| 481 | scrobble_state: &Arc<Mutex<ScrobbleState>>, | ||
| 253 | ) -> String { | 482 | ) -> String { |
| 254 | match cmd { | 483 | match cmd { |
| 255 | "status" => { | 484 | "status" => { |
| @@ -291,6 +520,9 @@ fn process_command( | |||
| 291 | } | 520 | } |
| 292 | } | 521 | } |
| 293 | "stop" => { | 522 | "stop" => { |
| 523 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 524 | try_scrobble_current(scrobbler, &mut s); | ||
| 525 | } | ||
| 294 | audio.stop(); | 526 | audio.stop(); |
| 295 | if let Ok(mut q) = queue.lock() { | 527 | if let Ok(mut q) = queue.lock() { |
| 296 | q.clear(); | 528 | q.clear(); |
| @@ -299,19 +531,57 @@ fn process_command( | |||
| 299 | "Stopped\n".to_string() | 531 | "Stopped\n".to_string() |
| 300 | } | 532 | } |
| 301 | "stop_daemon" => { | 533 | "stop_daemon" => { |
| 302 | // Signal the daemon to shut down | ||
| 303 | shutdown_flag.store(true, Ordering::SeqCst); | 534 | shutdown_flag.store(true, Ordering::SeqCst); |
| 304 | "Shutting down\n".to_string() | 535 | "Shutting down\n".to_string() |
| 305 | } | 536 | } |
| 537 | "reload" | "validate" => { | ||
| 538 | if let Ok(config) = Config::load() { | ||
| 539 | let temp_scrobbler = Scrobbler::new( | ||
| 540 | config.lastfm_api_key.clone(), | ||
| 541 | config.lastfm_api_secret.clone(), | ||
| 542 | config.lastfm_session_key.clone(), | ||
| 543 | config.listenbrainz_token.clone(), | ||
| 544 | ); | ||
| 545 | let result = match temp_scrobbler.validate_listenbrainz_token() { | ||
| 546 | Ok(Some(user)) => { | ||
| 547 | notification::notify_change(&format!( | ||
| 548 | "Logged in to ListenBrainz as {}", | ||
| 549 | user | ||
| 550 | )); | ||
| 551 | format!("ListenBrainz token valid (user: {})\n", user) | ||
| 552 | } | ||
| 553 | Ok(None) => { | ||
| 554 | notification::notify_error("Invalid ListenBrainz token"); | ||
| 555 | "ListenBrainz token invalid\n".to_string() | ||
| 556 | } | ||
| 557 | Err(e) => { | ||
| 558 | notification::notify_error(&format!("ListenBrainz validation: {}", e)); | ||
| 559 | format!("ListenBrainz validation error: {}\n", e) | ||
| 560 | } | ||
| 561 | }; | ||
| 562 | // Replace shared scrobbler state (only on reload, both is fine) | ||
| 563 | if let Ok(mut s) = scrobbler.write() { | ||
| 564 | *s = temp_scrobbler; | ||
| 565 | } | ||
| 566 | result | ||
| 567 | } else { | ||
| 568 | "Failed to load config\n".to_string() | ||
| 569 | } | ||
| 570 | } | ||
| 306 | "next" => { | 571 | "next" => { |
| 572 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 573 | try_scrobble_current(scrobbler, &mut s); | ||
| 574 | } | ||
| 307 | if let Ok(mut q) = queue.lock() { | 575 | if let Ok(mut q) = queue.lock() { |
| 308 | if let Some(next_track) = q.next() { | 576 | if let Some(next_track) = q.next() { |
| 309 | audio.play(next_track.clone()); | 577 | audio.play(next_track.clone()); |
| 578 | let track = Track::from_path(next_track.clone()); | ||
| 579 | send_now_playing(scrobbler, &track); | ||
| 580 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 581 | s.start(track.clone()); | ||
| 582 | } | ||
| 310 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 583 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 311 | notification::notify_track( | 584 | notification::notify_track(&track, fmt.as_deref()); |
| 312 | &Track::from_path(next_track.clone()), | ||
| 313 | fmt.as_deref(), | ||
| 314 | ); | ||
| 315 | "Playing next track\n".to_string() | 585 | "Playing next track\n".to_string() |
| 316 | } else { | 586 | } else { |
| 317 | audio.stop(); | 587 | audio.stop(); |
| @@ -323,14 +593,19 @@ fn process_command( | |||
| 323 | } | 593 | } |
| 324 | } | 594 | } |
| 325 | "previous" => { | 595 | "previous" => { |
| 596 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 597 | try_scrobble_current(scrobbler, &mut s); | ||
| 598 | } | ||
| 326 | if let Ok(mut q) = queue.lock() { | 599 | if let Ok(mut q) = queue.lock() { |
| 327 | if let Some(prev_track) = q.previous() { | 600 | if let Some(prev_track) = q.previous() { |
| 328 | audio.play(prev_track.clone()); | 601 | audio.play(prev_track.clone()); |
| 602 | let track = Track::from_path(prev_track.clone()); | ||
| 603 | send_now_playing(scrobbler, &track); | ||
| 604 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 605 | s.start(track.clone()); | ||
| 606 | } | ||
| 329 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 607 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); |
| 330 | notification::notify_track( | 608 | notification::notify_track(&track, fmt.as_deref()); |
| 331 | &Track::from_path(prev_track.clone()), | ||
| 332 | fmt.as_deref(), | ||
| 333 | ); | ||
| 334 | "Playing previous track\n".to_string() | 609 | "Playing previous track\n".to_string() |
| 335 | } else { | 610 | } else { |
| 336 | "No previous track\n".to_string() | 611 | "No previous track\n".to_string() |
| @@ -408,6 +683,9 @@ fn process_command( | |||
| 408 | let was_empty = q.is_empty(); | 683 | let was_empty = q.is_empty(); |
| 409 | match position { | 684 | match position { |
| 410 | "now" => { | 685 | "now" => { |
| 686 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 687 | try_scrobble_current(scrobbler, &mut s); | ||
| 688 | } | ||
| 411 | q.insert_after_current(path.clone()); | 689 | q.insert_after_current(path.clone()); |
| 412 | if !was_empty { | 690 | if !was_empty { |
| 413 | let new_pos = q.position() + 1; | 691 | let new_pos = q.position() + 1; |
| @@ -415,6 +693,10 @@ fn process_command( | |||
| 415 | } | 693 | } |
| 416 | if let Some(track) = q.track_at(q.position()) { | 694 | if let Some(track) = q.track_at(q.position()) { |
| 417 | audio.play(track.path.clone()); | 695 | audio.play(track.path.clone()); |
| 696 | send_now_playing(scrobbler, track); | ||
| 697 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 698 | s.start(track.clone()); | ||
| 699 | } | ||
| 418 | let fmt = | 700 | let fmt = |
| 419 | Config::load().ok().and_then(|c| c.display_format.clone()); | 701 | Config::load().ok().and_then(|c| c.display_format.clone()); |
| 420 | notification::notify_track(track, fmt.as_deref()); | 702 | notification::notify_track(track, fmt.as_deref()); |
| @@ -432,6 +714,10 @@ fn process_command( | |||
| 432 | if was_empty && !was_playing { | 714 | if was_empty && !was_playing { |
| 433 | let track = Track::from_path(path.clone()); | 715 | let track = Track::from_path(path.clone()); |
| 434 | audio.play(path); | 716 | audio.play(path); |
| 717 | send_now_playing(scrobbler, &track); | ||
| 718 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 719 | s.start(track.clone()); | ||
| 720 | } | ||
| 435 | let fmt = | 721 | let fmt = |
| 436 | Config::load().ok().and_then(|c| c.display_format.clone()); | 722 | Config::load().ok().and_then(|c| c.display_format.clone()); |
| 437 | notification::notify_track(&track, fmt.as_deref()); | 723 | notification::notify_track(&track, fmt.as_deref()); |
| @@ -451,6 +737,9 @@ fn process_command( | |||
| 451 | let was_empty = q.is_empty(); | 737 | let was_empty = q.is_empty(); |
| 452 | match position { | 738 | match position { |
| 453 | "now" => { | 739 | "now" => { |
| 740 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 741 | try_scrobble_current(scrobbler, &mut s); | ||
| 742 | } | ||
| 454 | for file in files.iter().rev() { | 743 | for file in files.iter().rev() { |
| 455 | q.insert_after_current(file.clone()); | 744 | q.insert_after_current(file.clone()); |
| 456 | } | 745 | } |
| @@ -460,6 +749,10 @@ fn process_command( | |||
| 460 | } | 749 | } |
| 461 | if let Some(track) = q.track_at(q.position()) { | 750 | if let Some(track) = q.track_at(q.position()) { |
| 462 | audio.play(track.path.clone()); | 751 | audio.play(track.path.clone()); |
| 752 | send_now_playing(scrobbler, track); | ||
| 753 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 754 | s.start(track.clone()); | ||
| 755 | } | ||
| 463 | let fmt = Config::load() | 756 | let fmt = Config::load() |
| 464 | .ok() | 757 | .ok() |
| 465 | .and_then(|c| c.display_format.clone()); | 758 | .and_then(|c| c.display_format.clone()); |
| @@ -491,6 +784,10 @@ fn process_command( | |||
| 491 | && let Some(track) = q.current() | 784 | && let Some(track) = q.current() |
| 492 | { | 785 | { |
| 493 | audio.play(track.path.clone()); | 786 | audio.play(track.path.clone()); |
| 787 | send_now_playing(scrobbler, track); | ||
| 788 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 789 | s.start(track.clone()); | ||
| 790 | } | ||
| 494 | let fmt = Config::load() | 791 | let fmt = Config::load() |
| 495 | .ok() | 792 | .ok() |
| 496 | .and_then(|c| c.display_format.clone()); | 793 | .and_then(|c| c.display_format.clone()); |
