diff options
Diffstat (limited to '')
| -rw-r--r-- | src/daemon.rs | 140 |
1 files changed, 127 insertions, 13 deletions
diff --git a/src/daemon.rs b/src/daemon.rs index 8f470c1..4f95820 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -954,6 +954,49 @@ fn resume_queue_track( | |||
| 954 | true | 954 | true |
| 955 | } | 955 | } |
| 956 | 956 | ||
| 957 | /// Parse a seek argument into an absolute position in seconds. | ||
| 958 | /// | ||
| 959 | /// Accepts `90`, `1:30`, `1:02:03`, and relative offsets `+15` / `-30`. | ||
| 960 | fn parse_seek(arg: &str, current: f64) -> Option<f64> { | ||
| 961 | let (sign, body) = match arg.as_bytes().first()? { | ||
| 962 | b'+' => (1.0, &arg[1..]), | ||
| 963 | b'-' => (-1.0, &arg[1..]), | ||
| 964 | _ => (0.0, arg), | ||
| 965 | }; | ||
| 966 | let value = parse_time(body)?; | ||
| 967 | Some(if sign == 0.0 { | ||
| 968 | value | ||
| 969 | } else { | ||
| 970 | (current + sign * value).max(0.0) | ||
| 971 | }) | ||
| 972 | } | ||
| 973 | |||
| 974 | /// Parse `SS`, `MM:SS`, or `HH:MM:SS` (fractions allowed) into seconds. | ||
| 975 | fn parse_time(text: &str) -> Option<f64> { | ||
| 976 | let mut total = 0.0f64; | ||
| 977 | let mut parts = 0usize; | ||
| 978 | for part in text.split(':') { | ||
| 979 | let value: f64 = part.trim().parse().ok()?; | ||
| 980 | if value < 0.0 { | ||
| 981 | return None; | ||
| 982 | } | ||
| 983 | total = total * 60.0 + value; | ||
| 984 | parts += 1; | ||
| 985 | } | ||
| 986 | (1..=3).contains(&parts).then_some(total) | ||
| 987 | } | ||
| 988 | |||
| 989 | /// `83.4` -> `1:23`, `3723` -> `1:02:03` | ||
| 990 | fn format_time(seconds: f64) -> String { | ||
| 991 | let secs = seconds.max(0.0) as u64; | ||
| 992 | let (hours, minutes, secs) = (secs / 3600, (secs % 3600) / 60, secs % 60); | ||
| 993 | if hours > 0 { | ||
| 994 | format!("{}:{:02}:{:02}", hours, minutes, secs) | ||
| 995 | } else { | ||
| 996 | format!("{}:{:02}", minutes, secs) | ||
| 997 | } | ||
| 998 | } | ||
| 999 | |||
| 957 | fn process_command( | 1000 | fn process_command( |
| 958 | cmd: &str, | 1001 | cmd: &str, |
| 959 | audio: &AudioEngine, | 1002 | audio: &AudioEngine, |
| @@ -964,20 +1007,38 @@ fn process_command( | |||
| 964 | ) -> String { | 1007 | ) -> String { |
| 965 | match cmd { | 1008 | match cmd { |
| 966 | "status" => { | 1009 | "status" => { |
| 967 | if audio.is_playing() { | 1010 | let current = audio.current_track().or_else(|| { |
| 968 | if let Some(track_path) = audio.current_track() { | 1011 | queue |
| 969 | let track = Track::from_path(track_path); | 1012 | .lock() |
| 970 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | 1013 | .ok() |
| 971 | let track_str = match fmt.as_deref() { | 1014 | .and_then(|q| q.current().map(|t| t.path.clone())) |
| 972 | Some(f) => track.format(f), | 1015 | }); |
| 973 | None => track.display_string(0, 1), | 1016 | let Some(track_path) = current else { |
| 974 | }; | 1017 | return "Stopped\n".to_string(); |
| 975 | format!("Playing\nTrack: {}\n", track_str) | 1018 | }; |
| 976 | } else { | 1019 | let state = if audio.is_playing() { |
| 977 | "Playing\n".to_string() | 1020 | "Playing" |
| 978 | } | ||
| 979 | } else { | 1021 | } else { |
| 980 | "Stopped\n".to_string() | 1022 | "Paused" |
| 1023 | }; | ||
| 1024 | let track = Track::from_path(track_path); | ||
| 1025 | let fmt = Config::load().ok().and_then(|c| c.display_format.clone()); | ||
| 1026 | let track_str = match fmt.as_deref() { | ||
| 1027 | Some(f) => track.format(f), | ||
| 1028 | None => track.display_string(0, 1), | ||
| 1029 | }; | ||
| 1030 | let position = format_time(audio.position()); | ||
| 1031 | match track.duration_secs { | ||
| 1032 | Some(duration) if duration > 0.0 => { | ||
| 1033 | format!( | ||
| 1034 | "{}\nTrack: {}\nPosition: {} / {}\n", | ||
| 1035 | state, | ||
| 1036 | track_str, | ||
| 1037 | position, | ||
| 1038 | format_time(duration) | ||
| 1039 | ) | ||
| 1040 | } | ||
| 1041 | _ => format!("{}\nTrack: {}\nPosition: {}\n", state, track_str, position), | ||
| 981 | } | 1042 | } |
| 982 | } | 1043 | } |
| 983 | "pause" => { | 1044 | "pause" => { |
| @@ -1014,6 +1075,20 @@ fn process_command( | |||
| 1014 | notification::notify_change("Stopped"); | 1075 | notification::notify_change("Stopped"); |
| 1015 | "Stopped\n".to_string() | 1076 | "Stopped\n".to_string() |
| 1016 | } | 1077 | } |
| 1078 | _ if cmd == "seek" || cmd.starts_with("seek ") => { | ||
| 1079 | let current = audio.position(); | ||
| 1080 | let arg = cmd[4..].trim(); | ||
| 1081 | if arg.is_empty() { | ||
| 1082 | return format!("Position: {}\n", format_time(current)); | ||
| 1083 | } | ||
| 1084 | match parse_seek(arg, current) { | ||
| 1085 | Some(target) => { | ||
| 1086 | audio.seek(target); | ||
| 1087 | format!("Seeking to {}\n", format_time(target)) | ||
| 1088 | } | ||
| 1089 | None => "Usage: rsp seek <seconds|MM:SS|[+|-]seconds>\n".to_string(), | ||
| 1090 | } | ||
| 1091 | } | ||
| 1017 | "stop_daemon" => { | 1092 | "stop_daemon" => { |
| 1018 | shutdown_flag.store(true, Ordering::SeqCst); | 1093 | shutdown_flag.store(true, Ordering::SeqCst); |
| 1019 | "Shutting down\n".to_string() | 1094 | "Shutting down\n".to_string() |
| @@ -1834,3 +1909,42 @@ fn cleanup_daemon(socket_path: &std::path::Path, pid_path: &std::path::Path) { | |||
| 1834 | std::fs::remove_file(socket_path).ok(); | 1909 | std::fs::remove_file(socket_path).ok(); |
| 1835 | std::fs::remove_file(pid_path).ok(); | 1910 | std::fs::remove_file(pid_path).ok(); |
| 1836 | } | 1911 | } |
| 1912 | |||
| 1913 | #[cfg(test)] | ||
| 1914 | mod tests { | ||
| 1915 | use super::*; | ||
| 1916 | |||
| 1917 | #[test] | ||
| 1918 | fn parses_absolute_seek_positions() { | ||
| 1919 | assert_eq!(parse_seek("90", 10.0), Some(90.0)); | ||
| 1920 | assert_eq!(parse_seek("1:30", 10.0), Some(90.0)); | ||
| 1921 | assert_eq!(parse_seek("1:02:03", 0.0), Some(3723.0)); | ||
| 1922 | assert_eq!(parse_seek("83.5", 0.0), Some(83.5)); | ||
| 1923 | } | ||
| 1924 | |||
| 1925 | #[test] | ||
| 1926 | fn parses_relative_seek_offsets() { | ||
| 1927 | assert_eq!(parse_seek("+15", 60.0), Some(75.0)); | ||
| 1928 | assert_eq!(parse_seek("-15", 60.0), Some(45.0)); | ||
| 1929 | assert_eq!( | ||
| 1930 | parse_seek("-60", 30.0), | ||
| 1931 | Some(0.0), | ||
| 1932 | "relative seeks never go below the start" | ||
| 1933 | ); | ||
| 1934 | } | ||
| 1935 | |||
| 1936 | #[test] | ||
| 1937 | fn rejects_bad_seek_positions() { | ||
| 1938 | assert_eq!(parse_seek("abc", 0.0), None); | ||
| 1939 | assert_eq!(parse_seek("", 0.0), None); | ||
| 1940 | assert_eq!(parse_seek("-", 0.0), None); | ||
| 1941 | assert_eq!(parse_seek("1:2:3:4", 0.0), None, "at most hours:min:sec"); | ||
| 1942 | } | ||
| 1943 | |||
| 1944 | #[test] | ||
| 1945 | fn formats_positions_for_status() { | ||
| 1946 | assert_eq!(format_time(0.0), "0:00"); | ||
| 1947 | assert_eq!(format_time(83.4), "1:23"); | ||
| 1948 | assert_eq!(format_time(3723.0), "1:02:03"); | ||
| 1949 | } | ||
| 1950 | } | ||
