diff options
| author | salmonik <raisersoft@proton.me> | 2026-05-27 20:46:33 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-05-27 20:46:33 +0200 |
| commit | 0ae9d89f18bf36407229798a3b4943604fb708e4 (patch) | |
| tree | 47fbc6ad1d22ddd575f9773d5e1d36dae7bd0815 /src/daemon.rs | |
| parent | play [track] [-a/--album] option (diff) | |
| download | rsplayer-0ae9d89f18bf36407229798a3b4943604fb708e4.tar.gz rsplayer-0ae9d89f18bf36407229798a3b4943604fb708e4.zip | |
Add internet streams support
Diffstat (limited to '')
| -rw-r--r-- | src/daemon.rs | 216 |
1 files changed, 149 insertions, 67 deletions
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 { | |||
| 34 | } | 34 | } |
| 35 | 35 | ||
| 36 | fn start(&mut self, track: Track) { | 36 | fn start(&mut self, track: Track) { |
| 37 | if track.is_stream { | ||
| 38 | crate::debug_eprintln(format_args!( | ||
| 39 | "scrobble: not tracking stream '{}'", | ||
| 40 | track.path.display() | ||
| 41 | )); | ||
| 42 | self.clear(); | ||
| 43 | return; | ||
| 44 | } | ||
| 37 | let ts = SystemTime::now() | 45 | let ts = SystemTime::now() |
| 38 | .duration_since(SystemTime::UNIX_EPOCH) | 46 | .duration_since(SystemTime::UNIX_EPOCH) |
| 39 | .unwrap_or_default() | 47 | .unwrap_or_default() |
| @@ -109,6 +117,7 @@ mod tests { | |||
| 109 | artist: Some("Artist".into()), | 117 | artist: Some("Artist".into()), |
| 110 | album: None, | 118 | album: None, |
| 111 | duration_secs: Some(200.0), | 119 | duration_secs: Some(200.0), |
| 120 | is_stream: false, | ||
| 112 | }; | 121 | }; |
| 113 | s.start(t); | 122 | s.start(t); |
| 114 | assert!(s.track.is_some()); | 123 | assert!(s.track.is_some()); |
| @@ -161,6 +170,13 @@ mod tests { | |||
| 161 | } | 170 | } |
| 162 | 171 | ||
| 163 | fn try_scrobble_current(scrobbler: &RwLock<Scrobbler>, state: &mut ScrobbleState) { | 172 | fn try_scrobble_current(scrobbler: &RwLock<Scrobbler>, state: &mut ScrobbleState) { |
| 173 | if let Some(ref track) = state.track | ||
| 174 | && track.is_stream | ||
| 175 | { | ||
| 176 | crate::debug_eprintln(format_args!("scrobble: skipping scrobble for stream track")); | ||
| 177 | state.clear(); | ||
| 178 | return; | ||
| 179 | } | ||
| 164 | if state.should_scrobble() | 180 | if state.should_scrobble() |
| 165 | && let (Some(track), Some(ts)) = (&state.track, state.timestamp) | 181 | && let (Some(track), Some(ts)) = (&state.track, state.timestamp) |
| 166 | { | 182 | { |
| @@ -189,6 +205,12 @@ fn try_scrobble_current(scrobbler: &RwLock<Scrobbler>, state: &mut ScrobbleState | |||
| 189 | } | 205 | } |
| 190 | 206 | ||
| 191 | fn send_now_playing(scrobbler: &RwLock<Scrobbler>, track: &Track) { | 207 | fn send_now_playing(scrobbler: &RwLock<Scrobbler>, track: &Track) { |
| 208 | if track.is_stream { | ||
| 209 | crate::debug_eprintln(format_args!( | ||
| 210 | "scrobble: skipping now_playing for stream track" | ||
| 211 | )); | ||
| 212 | return; | ||
| 213 | } | ||
| 192 | let artist = track.artist.as_deref().unwrap_or("(unknown artist)"); | 214 | let artist = track.artist.as_deref().unwrap_or("(unknown artist)"); |
| 193 | let title = track.title.as_deref().unwrap_or("(unknown title)"); | 215 | let title = track.title.as_deref().unwrap_or("(unknown title)"); |
| 194 | let album = track.album.as_deref(); | 216 | let album = track.album.as_deref(); |
| @@ -845,16 +867,18 @@ fn process_command( | |||
| 845 | return "Usage: play [now|next|last] <path>\n".to_string(); | 867 | return "Usage: play [now|next|last] <path>\n".to_string(); |
| 846 | } | 868 | } |
| 847 | 869 | ||
| 848 | let path = std::path::PathBuf::from(path_str.clone()); | 870 | let is_url = path_str.starts_with("http://") || path_str.starts_with("https://"); |
| 849 | if path.is_file() { | 871 | |
| 872 | if is_url { | ||
| 850 | if let Ok(mut q) = queue.lock() { | 873 | if let Ok(mut q) = queue.lock() { |
| 851 | let was_empty = q.is_empty(); | 874 | let was_empty = q.is_empty(); |
| 875 | let track = Track::from_url(&path_str); | ||
| 852 | match position { | 876 | match position { |
| 853 | "now" => { | 877 | "now" => { |
| 854 | if let Ok(mut s) = scrobble_state.lock() { | 878 | if let Ok(mut s) = scrobble_state.lock() { |
| 855 | try_scrobble_current(scrobbler, &mut s); | 879 | try_scrobble_current(scrobbler, &mut s); |
| 856 | } | 880 | } |
| 857 | q.insert_after_current(path.clone()); | 881 | q.push_track(track.clone()); |
| 858 | if !was_empty { | 882 | if !was_empty { |
| 859 | let new_pos = q.position() + 1; | 883 | let new_pos = q.position() + 1; |
| 860 | q.set_position(new_pos); | 884 | q.set_position(new_pos); |
| @@ -872,84 +896,103 @@ fn process_command( | |||
| 872 | format!("Playing now: {}\n", path_str) | 896 | format!("Playing now: {}\n", path_str) |
| 873 | } | 897 | } |
| 874 | "next" => { | 898 | "next" => { |
| 875 | q.insert_after_current(path.clone()); | 899 | q.push_track(track); |
| 876 | format!("Queued next: {}\n", path_str) | 900 | format!("Queued next: {}\n", path_str) |
| 877 | } | 901 | } |
| 878 | _ => { | 902 | _ => { |
| 879 | let was_empty = q.is_empty(); | ||
| 880 | let was_playing = audio.is_playing(); | 903 | let was_playing = audio.is_playing(); |
| 881 | q.add_track(path.clone()); | 904 | q.push_track(track.clone()); |
| 882 | if was_empty && !was_playing { | 905 | if was_empty |
| 883 | let track = Track::from_path(path.clone()); | 906 | && !was_playing |
| 884 | audio.play(path); | 907 | && let Some(track) = q.current() |
| 885 | send_now_playing(scrobbler, &track); | 908 | { |
| 909 | audio.play(track.path.clone()); | ||
| 910 | send_now_playing(scrobbler, track); | ||
| 886 | if let Ok(mut s) = scrobble_state.lock() { | 911 | if let Ok(mut s) = scrobble_state.lock() { |
| 887 | s.start(track.clone()); | 912 | s.start(track.clone()); |
| 888 | } | 913 | } |
| 889 | let fmt = | 914 | let fmt = |
| 890 | Config::load().ok().and_then(|c| c.display_format.clone()); | 915 | Config::load().ok().and_then(|c| c.display_format.clone()); |
| 891 | notification::notify_track(&track, fmt.as_deref()); | 916 | notification::notify_track(track, fmt.as_deref()); |
| 892 | format!("Playing: {}\n", path_str) | ||
| 893 | } else { | ||
| 894 | format!("Queued: {}\n", path_str) | ||
| 895 | } | 917 | } |
| 918 | format!("Queued: {}\n", path_str) | ||
| 896 | } | 919 | } |
| 897 | } | 920 | } |
| 898 | } else { | 921 | } else { |
| 899 | "Error accessing queue\n".to_string() | 922 | "Error accessing queue\n".to_string() |
| 900 | } | 923 | } |
| 901 | } else if path.is_dir() { | 924 | } else { |
| 902 | match find_audio_in_dir(&path) { | 925 | let path = std::path::PathBuf::from(path_str.clone()); |
| 903 | Some(files) if !files.is_empty() => { | 926 | if path.is_file() { |
| 904 | if let Ok(mut q) = queue.lock() { | 927 | if let Ok(mut q) = queue.lock() { |
| 905 | let was_empty = q.is_empty(); | 928 | let was_empty = q.is_empty(); |
| 906 | match position { | 929 | match position { |
| 907 | "now" => { | 930 | "now" => { |
| 931 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 932 | try_scrobble_current(scrobbler, &mut s); | ||
| 933 | } | ||
| 934 | q.insert_after_current(path.clone()); | ||
| 935 | if !was_empty { | ||
| 936 | let new_pos = q.position() + 1; | ||
| 937 | q.set_position(new_pos); | ||
| 938 | } | ||
| 939 | if let Some(track) = q.track_at(q.position()) { | ||
| 940 | audio.play(track.path.clone()); | ||
| 941 | send_now_playing(scrobbler, track); | ||
| 908 | if let Ok(mut s) = scrobble_state.lock() { | 942 | if let Ok(mut s) = scrobble_state.lock() { |
| 909 | try_scrobble_current(scrobbler, &mut s); | 943 | s.start(track.clone()); |
| 910 | } | ||
| 911 | for file in files.iter().rev() { | ||
| 912 | q.insert_after_current(file.clone()); | ||
| 913 | } | ||
| 914 | if !was_empty { | ||
| 915 | let new_pos = q.position() + 1; | ||
| 916 | q.set_position(new_pos); | ||
| 917 | } | ||
| 918 | if let Some(track) = q.track_at(q.position()) { | ||
| 919 | audio.play(track.path.clone()); | ||
| 920 | send_now_playing(scrobbler, track); | ||
| 921 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 922 | s.start(track.clone()); | ||
| 923 | } | ||
| 924 | let fmt = Config::load() | ||
| 925 | .ok() | ||
| 926 | .and_then(|c| c.display_format.clone()); | ||
| 927 | notification::notify_track(track, fmt.as_deref()); | ||
| 928 | } | 944 | } |
| 929 | format!( | 945 | let fmt = |
| 930 | "Playing now: {} tracks from {}\n", | 946 | Config::load().ok().and_then(|c| c.display_format.clone()); |
| 931 | files.len(), | 947 | notification::notify_track(track, fmt.as_deref()); |
| 932 | path_str | ||
| 933 | ) | ||
| 934 | } | 948 | } |
| 935 | "next" => { | 949 | format!("Playing now: {}\n", path_str) |
| 936 | for file in files.iter().rev() { | 950 | } |
| 937 | q.insert_after_current(file.clone()); | 951 | "next" => { |
| 952 | q.insert_after_current(path.clone()); | ||
| 953 | format!("Queued next: {}\n", path_str) | ||
| 954 | } | ||
| 955 | _ => { | ||
| 956 | let was_empty = q.is_empty(); | ||
| 957 | let was_playing = audio.is_playing(); | ||
| 958 | q.add_track(path.clone()); | ||
| 959 | if was_empty && !was_playing { | ||
| 960 | let track = Track::from_path(path.clone()); | ||
| 961 | audio.play(path); | ||
| 962 | send_now_playing(scrobbler, &track); | ||
| 963 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 964 | s.start(track.clone()); | ||
| 938 | } | 965 | } |
| 939 | format!( | 966 | let fmt = |
| 940 | "Queued next: {} tracks from {}\n", | 967 | Config::load().ok().and_then(|c| c.display_format.clone()); |
| 941 | files.len(), | 968 | notification::notify_track(&track, fmt.as_deref()); |
| 942 | path_str | 969 | format!("Playing: {}\n", path_str) |
| 943 | ) | 970 | } else { |
| 971 | format!("Queued: {}\n", path_str) | ||
| 944 | } | 972 | } |
| 945 | _ => { | 973 | } |
| 946 | let was_playing = audio.is_playing(); | 974 | } |
| 947 | for file in &files { | 975 | } else { |
| 948 | q.add_track(file.clone()); | 976 | "Error accessing queue\n".to_string() |
| 949 | } | 977 | } |
| 950 | if was_empty && !was_playing { | 978 | } else if path.is_dir() { |
| 951 | q.shuffle_initial(); | 979 | match find_audio_in_dir(&path) { |
| 952 | if let Some(track) = q.current() { | 980 | Some(files) if !files.is_empty() => { |
| 981 | if let Ok(mut q) = queue.lock() { | ||
| 982 | let was_empty = q.is_empty(); | ||
| 983 | match position { | ||
| 984 | "now" => { | ||
| 985 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 986 | try_scrobble_current(scrobbler, &mut s); | ||
| 987 | } | ||
| 988 | for file in files.iter().rev() { | ||
| 989 | q.insert_after_current(file.clone()); | ||
| 990 | } | ||
| 991 | if !was_empty { | ||
| 992 | let new_pos = q.position() + 1; | ||
| 993 | q.set_position(new_pos); | ||
| 994 | } | ||
| 995 | if let Some(track) = q.track_at(q.position()) { | ||
| 953 | audio.play(track.path.clone()); | 996 | audio.play(track.path.clone()); |
| 954 | send_now_playing(scrobbler, track); | 997 | send_now_playing(scrobbler, track); |
| 955 | if let Ok(mut s) = scrobble_state.lock() { | 998 | if let Ok(mut s) = scrobble_state.lock() { |
| @@ -960,18 +1003,57 @@ fn process_command( | |||
| 960 | .and_then(|c| c.display_format.clone()); | 1003 | .and_then(|c| c.display_format.clone()); |
| 961 | notification::notify_track(track, fmt.as_deref()); | 1004 | notification::notify_track(track, fmt.as_deref()); |
| 962 | } | 1005 | } |
| 1006 | format!( | ||
| 1007 | "Playing now: {} tracks from {}\n", | ||
| 1008 | files.len(), | ||
| 1009 | path_str | ||
| 1010 | ) | ||
| 1011 | } | ||
| 1012 | "next" => { | ||
| 1013 | for file in files.iter().rev() { | ||
| 1014 | q.insert_after_current(file.clone()); | ||
| 1015 | } | ||
| 1016 | format!( | ||
| 1017 | "Queued next: {} tracks from {}\n", | ||
| 1018 | files.len(), | ||
| 1019 | path_str | ||
| 1020 | ) | ||
| 1021 | } | ||
| 1022 | _ => { | ||
| 1023 | let was_playing = audio.is_playing(); | ||
| 1024 | for file in &files { | ||
| 1025 | q.add_track(file.clone()); | ||
| 1026 | } | ||
| 1027 | if was_empty && !was_playing { | ||
| 1028 | q.shuffle_initial(); | ||
| 1029 | if let Some(track) = q.current() { | ||
| 1030 | audio.play(track.path.clone()); | ||
| 1031 | send_now_playing(scrobbler, track); | ||
| 1032 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 1033 | s.start(track.clone()); | ||
| 1034 | } | ||
| 1035 | let fmt = Config::load() | ||
| 1036 | .ok() | ||
| 1037 | .and_then(|c| c.display_format.clone()); | ||
| 1038 | notification::notify_track(track, fmt.as_deref()); | ||
| 1039 | } | ||
| 1040 | } | ||
| 1041 | format!( | ||
| 1042 | "Queued {} tracks from: {}\n", | ||
| 1043 | files.len(), | ||
| 1044 | path_str | ||
| 1045 | ) | ||
| 963 | } | 1046 | } |
| 964 | format!("Queued {} tracks from: {}\n", files.len(), path_str) | ||
| 965 | } | 1047 | } |
| 1048 | } else { | ||
| 1049 | "Error accessing queue\n".to_string() | ||
| 966 | } | 1050 | } |
| 967 | } else { | ||
| 968 | "Error accessing queue\n".to_string() | ||
| 969 | } | 1051 | } |
| 1052 | _ => format!("Error: No audio files found in '{}'\n", path_str), | ||
| 970 | } | 1053 | } |
| 971 | _ => format!("Error: No audio files found in '{}'\n", path_str), | 1054 | } else { |
| 1055 | format!("File not found: {}\n", path_str) | ||
| 972 | } | 1056 | } |
| 973 | } else { | ||
| 974 | format!("File not found: {}\n", path_str) | ||
| 975 | } | 1057 | } |
| 976 | } | 1058 | } |
| 977 | _ if cmd == "volume" => { | 1059 | _ if cmd == "volume" => { |
