From 0ae9d89f18bf36407229798a3b4943604fb708e4 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 27 May 2026 20:46:33 +0200 Subject: Add internet streams support --- src/daemon.rs | 216 ++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 149 insertions(+), 67 deletions(-) (limited to 'src/daemon.rs') diff --git a/src/daemon.rs b/src/daemon.rs index 20f4e7e..c794495 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -34,6 +34,14 @@ impl ScrobbleState { } fn start(&mut self, track: Track) { + if track.is_stream { + crate::debug_eprintln(format_args!( + "scrobble: not tracking stream '{}'", + track.path.display() + )); + self.clear(); + return; + } let ts = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap_or_default() @@ -109,6 +117,7 @@ mod tests { artist: Some("Artist".into()), album: None, duration_secs: Some(200.0), + is_stream: false, }; s.start(t); assert!(s.track.is_some()); @@ -161,6 +170,13 @@ mod tests { } 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(); + return; + } if state.should_scrobble() && let (Some(track), Some(ts)) = (&state.track, state.timestamp) { @@ -189,6 +205,12 @@ fn try_scrobble_current(scrobbler: &RwLock, state: &mut ScrobbleState } 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(); @@ -845,16 +867,18 @@ fn process_command( return "Usage: play [now|next|last] \n".to_string(); } - let path = std::path::PathBuf::from(path_str.clone()); - if path.is_file() { + let is_url = path_str.starts_with("http://") || path_str.starts_with("https://"); + + if is_url { if let Ok(mut q) = queue.lock() { let was_empty = q.is_empty(); + let track = Track::from_url(&path_str); match position { "now" => { if let Ok(mut s) = scrobble_state.lock() { try_scrobble_current(scrobbler, &mut s); } - q.insert_after_current(path.clone()); + q.push_track(track.clone()); if !was_empty { let new_pos = q.position() + 1; q.set_position(new_pos); @@ -872,84 +896,103 @@ fn process_command( format!("Playing now: {}\n", path_str) } "next" => { - q.insert_after_current(path.clone()); + q.push_track(track); format!("Queued next: {}\n", path_str) } _ => { - let was_empty = q.is_empty(); let was_playing = audio.is_playing(); - q.add_track(path.clone()); - if was_empty && !was_playing { - let track = Track::from_path(path.clone()); - audio.play(path); - send_now_playing(scrobbler, &track); + q.push_track(track.clone()); + if was_empty + && !was_playing + && let Some(track) = q.current() + { + audio.play(track.path.clone()); + send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { s.start(track.clone()); } let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); - notification::notify_track(&track, fmt.as_deref()); - format!("Playing: {}\n", path_str) - } else { - format!("Queued: {}\n", path_str) + notification::notify_track(track, fmt.as_deref()); } + format!("Queued: {}\n", path_str) } } } else { "Error accessing queue\n".to_string() } - } else if path.is_dir() { - match find_audio_in_dir(&path) { - Some(files) if !files.is_empty() => { - if let Ok(mut q) = queue.lock() { - let was_empty = q.is_empty(); - match position { - "now" => { + } else { + let path = std::path::PathBuf::from(path_str.clone()); + if path.is_file() { + if let Ok(mut q) = queue.lock() { + let was_empty = q.is_empty(); + match position { + "now" => { + if let Ok(mut s) = scrobble_state.lock() { + try_scrobble_current(scrobbler, &mut s); + } + q.insert_after_current(path.clone()); + if !was_empty { + let new_pos = q.position() + 1; + q.set_position(new_pos); + } + if let Some(track) = q.track_at(q.position()) { + audio.play(track.path.clone()); + send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { - try_scrobble_current(scrobbler, &mut s); - } - for file in files.iter().rev() { - q.insert_after_current(file.clone()); - } - if !was_empty { - let new_pos = q.position() + 1; - q.set_position(new_pos); - } - if let Some(track) = q.track_at(q.position()) { - audio.play(track.path.clone()); - send_now_playing(scrobbler, track); - if let Ok(mut s) = scrobble_state.lock() { - s.start(track.clone()); - } - let fmt = Config::load() - .ok() - .and_then(|c| c.display_format.clone()); - notification::notify_track(track, fmt.as_deref()); + s.start(track.clone()); } - format!( - "Playing now: {} tracks from {}\n", - files.len(), - path_str - ) + let fmt = + Config::load().ok().and_then(|c| c.display_format.clone()); + notification::notify_track(track, fmt.as_deref()); } - "next" => { - for file in files.iter().rev() { - q.insert_after_current(file.clone()); + format!("Playing now: {}\n", path_str) + } + "next" => { + q.insert_after_current(path.clone()); + format!("Queued next: {}\n", path_str) + } + _ => { + let was_empty = q.is_empty(); + let was_playing = audio.is_playing(); + q.add_track(path.clone()); + if was_empty && !was_playing { + let track = Track::from_path(path.clone()); + audio.play(path); + send_now_playing(scrobbler, &track); + if let Ok(mut s) = scrobble_state.lock() { + s.start(track.clone()); } - format!( - "Queued next: {} tracks from {}\n", - files.len(), - path_str - ) + let fmt = + Config::load().ok().and_then(|c| c.display_format.clone()); + notification::notify_track(&track, fmt.as_deref()); + format!("Playing: {}\n", path_str) + } else { + format!("Queued: {}\n", path_str) } - _ => { - let was_playing = audio.is_playing(); - for file in &files { - q.add_track(file.clone()); - } - if was_empty && !was_playing { - q.shuffle_initial(); - if let Some(track) = q.current() { + } + } + } else { + "Error accessing queue\n".to_string() + } + } else if path.is_dir() { + match find_audio_in_dir(&path) { + Some(files) if !files.is_empty() => { + if let Ok(mut q) = queue.lock() { + let was_empty = q.is_empty(); + match position { + "now" => { + if let Ok(mut s) = scrobble_state.lock() { + try_scrobble_current(scrobbler, &mut s); + } + for file in files.iter().rev() { + q.insert_after_current(file.clone()); + } + if !was_empty { + let new_pos = q.position() + 1; + q.set_position(new_pos); + } + if let Some(track) = q.track_at(q.position()) { audio.play(track.path.clone()); send_now_playing(scrobbler, track); if let Ok(mut s) = scrobble_state.lock() { @@ -960,18 +1003,57 @@ fn process_command( .and_then(|c| c.display_format.clone()); notification::notify_track(track, fmt.as_deref()); } + format!( + "Playing now: {} tracks from {}\n", + files.len(), + path_str + ) + } + "next" => { + for file in files.iter().rev() { + q.insert_after_current(file.clone()); + } + format!( + "Queued next: {} tracks from {}\n", + files.len(), + path_str + ) + } + _ => { + let was_playing = audio.is_playing(); + for file in &files { + q.add_track(file.clone()); + } + if was_empty && !was_playing { + q.shuffle_initial(); + if let Some(track) = q.current() { + audio.play(track.path.clone()); + send_now_playing(scrobbler, track); + if let Ok(mut s) = scrobble_state.lock() { + s.start(track.clone()); + } + let fmt = Config::load() + .ok() + .and_then(|c| c.display_format.clone()); + notification::notify_track(track, fmt.as_deref()); + } + } + format!( + "Queued {} tracks from: {}\n", + files.len(), + path_str + ) } - format!("Queued {} tracks from: {}\n", files.len(), path_str) } + } else { + "Error accessing queue\n".to_string() } - } else { - "Error accessing queue\n".to_string() } + _ => format!("Error: No audio files found in '{}'\n", path_str), } - _ => format!("Error: No audio files found in '{}'\n", path_str), + } else { + format!("File not found: {}\n", path_str) } - } else { - format!("File not found: {}\n", path_str) } } _ if cmd == "volume" => { -- cgit v1.2.3