diff options
| author | salmonik <raisersoft@proton.me> | 2026-06-06 19:48:22 +0300 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-06-06 19:48:22 +0300 |
| commit | d347d89dfeef54d648ffdea054897617c0016509 (patch) | |
| tree | 0ea0e25e2a8e9bcdd8c06ffadbe29721a7f07f77 /src/daemon.rs | |
| parent | dirty, broken commit (diff) | |
| download | rsplayer-d347d89dfeef54d648ffdea054897617c0016509.tar.gz rsplayer-d347d89dfeef54d648ffdea054897617c0016509.zip | |
correctly working auto completion for db
Diffstat (limited to '')
| -rw-r--r-- | src/daemon.rs | 113 |
1 files changed, 31 insertions, 82 deletions
diff --git a/src/daemon.rs b/src/daemon.rs index d34305a..5ad6654 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -18,14 +18,20 @@ use std::sync::{Arc, Mutex, RwLock}; | |||
| 18 | use std::thread; | 18 | use std::thread; |
| 19 | use std::time::{Duration, Instant, SystemTime}; | 19 | use std::time::{Duration, Instant, SystemTime}; |
| 20 | 20 | ||
| 21 | struct ScrobbleState { | 21 | pub struct ScrobbleState { |
| 22 | track: Option<Track>, | 22 | pub track: Option<Track>, |
| 23 | start_time: Option<Instant>, | 23 | pub start_time: Option<Instant>, |
| 24 | timestamp: Option<u64>, | 24 | pub timestamp: Option<u64>, |
| 25 | } | ||
| 26 | |||
| 27 | impl Default for ScrobbleState { | ||
| 28 | fn default() -> Self { | ||
| 29 | Self::new() | ||
| 30 | } | ||
| 25 | } | 31 | } |
| 26 | 32 | ||
| 27 | impl ScrobbleState { | 33 | impl ScrobbleState { |
| 28 | fn new() -> Self { | 34 | pub fn new() -> Self { |
| 29 | ScrobbleState { | 35 | ScrobbleState { |
| 30 | track: None, | 36 | track: None, |
| 31 | start_time: None, | 37 | start_time: None, |
| @@ -33,7 +39,7 @@ impl ScrobbleState { | |||
| 33 | } | 39 | } |
| 34 | } | 40 | } |
| 35 | 41 | ||
| 36 | fn start(&mut self, track: Track) { | 42 | pub fn start(&mut self, track: Track) { |
| 37 | if track.is_stream { | 43 | if track.is_stream { |
| 38 | crate::debug_eprintln(format_args!( | 44 | crate::debug_eprintln(format_args!( |
| 39 | "scrobble: not tracking stream '{}'", | 45 | "scrobble: not tracking stream '{}'", |
| @@ -57,7 +63,7 @@ impl ScrobbleState { | |||
| 57 | self.timestamp = Some(ts); | 63 | self.timestamp = Some(ts); |
| 58 | } | 64 | } |
| 59 | 65 | ||
| 60 | fn should_scrobble(&self) -> bool { | 66 | pub fn should_scrobble(&self) -> bool { |
| 61 | let (Some(track), Some(start)) = (&self.track, self.start_time) else { | 67 | let (Some(track), Some(start)) = (&self.track, self.start_time) else { |
| 62 | crate::debug_eprintln(format_args!( | 68 | crate::debug_eprintln(format_args!( |
| 63 | "scrobble: should_scrobble false — no track or start_time" | 69 | "scrobble: should_scrobble false — no track or start_time" |
| @@ -78,7 +84,7 @@ impl ScrobbleState { | |||
| 78 | result | 84 | result |
| 79 | } | 85 | } |
| 80 | 86 | ||
| 81 | fn should_scrobble_with_params(elapsed: u64, duration_secs: Option<f64>) -> bool { | 87 | pub fn should_scrobble_with_params(elapsed: u64, duration_secs: Option<f64>) -> bool { |
| 82 | if elapsed < 30 { | 88 | if elapsed < 30 { |
| 83 | return false; | 89 | return false; |
| 84 | } | 90 | } |
| @@ -89,86 +95,13 @@ impl ScrobbleState { | |||
| 89 | elapsed >= threshold | 95 | elapsed >= threshold |
| 90 | } | 96 | } |
| 91 | 97 | ||
| 92 | fn clear(&mut self) { | 98 | pub fn clear(&mut self) { |
| 93 | self.track = None; | 99 | self.track = None; |
| 94 | self.start_time = None; | 100 | self.start_time = None; |
| 95 | self.timestamp = None; | 101 | self.timestamp = None; |
| 96 | } | 102 | } |
| 97 | } | 103 | } |
| 98 | 104 | ||
| 99 | #[cfg(test)] | ||
| 100 | mod tests { | ||
| 101 | use super::*; | ||
| 102 | |||
| 103 | #[test] | ||
| 104 | fn test_scrobble_state_new() { | ||
| 105 | let s = ScrobbleState::new(); | ||
| 106 | assert!(s.track.is_none()); | ||
| 107 | assert!(s.start_time.is_none()); | ||
| 108 | assert!(s.timestamp.is_none()); | ||
| 109 | } | ||
| 110 | |||
| 111 | #[test] | ||
| 112 | fn test_scrobble_state_clear() { | ||
| 113 | let mut s = ScrobbleState::new(); | ||
| 114 | let t = Track { | ||
| 115 | path: PathBuf::from("/test/song.mp3"), | ||
| 116 | title: Some("Test".into()), | ||
| 117 | artist: Some("Artist".into()), | ||
| 118 | album: None, | ||
| 119 | duration_secs: Some(200.0), | ||
| 120 | is_stream: false, | ||
| 121 | }; | ||
| 122 | s.start(t); | ||
| 123 | assert!(s.track.is_some()); | ||
| 124 | s.clear(); | ||
| 125 | assert!(s.track.is_none()); | ||
| 126 | assert!(s.start_time.is_none()); | ||
| 127 | assert!(s.timestamp.is_none()); | ||
| 128 | } | ||
| 129 | |||
| 130 | #[test] | ||
| 131 | fn test_should_scrobble_without_track_returns_false() { | ||
| 132 | assert!(!ScrobbleState::new().should_scrobble()); | ||
| 133 | } | ||
| 134 | |||
| 135 | #[test] | ||
| 136 | fn test_should_scrobble_threshold_below_30_seconds() { | ||
| 137 | assert!(!ScrobbleState::should_scrobble_with_params(29, Some(200.0))); | ||
| 138 | } | ||
| 139 | |||
| 140 | #[test] | ||
| 141 | fn test_should_scrobble_threshold_half_duration() { | ||
| 142 | assert!(ScrobbleState::should_scrobble_with_params(100, Some(200.0))); | ||
| 143 | } | ||
| 144 | |||
| 145 | #[test] | ||
| 146 | fn test_should_scrobble_threshold_below_half_duration() { | ||
| 147 | assert!(!ScrobbleState::should_scrobble_with_params(80, Some(200.0))); | ||
| 148 | } | ||
| 149 | |||
| 150 | #[test] | ||
| 151 | fn test_should_scrobble_threshold_capped_at_4min() { | ||
| 152 | assert!(ScrobbleState::should_scrobble_with_params(240, Some(600.0))); | ||
| 153 | assert!(!ScrobbleState::should_scrobble_with_params( | ||
| 154 | 239, | ||
| 155 | Some(600.0) | ||
| 156 | )); | ||
| 157 | } | ||
| 158 | |||
| 159 | #[test] | ||
| 160 | fn test_should_scrobble_no_duration_requires_4min() { | ||
| 161 | assert!(ScrobbleState::should_scrobble_with_params(240, None)); | ||
| 162 | assert!(!ScrobbleState::should_scrobble_with_params(239, None)); | ||
| 163 | } | ||
| 164 | |||
| 165 | #[test] | ||
| 166 | fn test_should_scrobble_short_song() { | ||
| 167 | assert!(ScrobbleState::should_scrobble_with_params(30, Some(60.0))); | ||
| 168 | assert!(!ScrobbleState::should_scrobble_with_params(15, Some(60.0))); | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | fn try_scrobble_current(scrobbler: &RwLock<Scrobbler>, state: &mut ScrobbleState) { | 105 | fn try_scrobble_current(scrobbler: &RwLock<Scrobbler>, state: &mut ScrobbleState) { |
| 173 | if let Some(ref track) = state.track | 106 | if let Some(ref track) = state.track |
| 174 | && track.is_stream | 107 | && track.is_stream |
| @@ -875,6 +808,22 @@ fn process_command( | |||
| 875 | // Strip trailing slash so autocompleted Artist/Album/ resolves cleanly | 808 | // Strip trailing slash so autocompleted Artist/Album/ resolves cleanly |
| 876 | let path_str = path_str.trim_end_matches('/').to_string(); | 809 | let path_str = path_str.trim_end_matches('/').to_string(); |
| 877 | 810 | ||
| 811 | // Unescape backslash sequences (e.g., \! → !) so shell escaping works | ||
| 812 | let path_str = { | ||
| 813 | let mut unescaped = String::with_capacity(path_str.len()); | ||
| 814 | let mut chars = path_str.chars(); | ||
| 815 | while let Some(c) = chars.next() { | ||
| 816 | if c == '\\' { | ||
| 817 | if let Some(next) = chars.next() { | ||
| 818 | unescaped.push(next); | ||
| 819 | } | ||
| 820 | } else { | ||
| 821 | unescaped.push(c); | ||
| 822 | } | ||
| 823 | } | ||
| 824 | unescaped | ||
| 825 | }; | ||
| 826 | |||
| 878 | if path_str.is_empty() { | 827 | if path_str.is_empty() { |
| 879 | return "Usage: play [now|next|last] <path>\n".to_string(); | 828 | return "Usage: play [now|next|last] <path>\n".to_string(); |
| 880 | } | 829 | } |
