cmus_notify/cmus/
query.rs

1use crate::cmus::events::CmusEvent;
2use crate::cmus::player_settings::PlayerSettings;
3use crate::cmus::{CmusError, Track};
4#[cfg(feature = "debug")]
5use log::{debug, info};
6use std::str::FromStr;
7
8/// This struct is used to store the row status response from cmus.
9/// So we don't parse it and take the time then we don't need it.
10/// We only parse it when we need it.
11#[derive(PartialEq, Default)]
12#[cfg_attr(feature = "debug", derive(Debug))]
13pub struct CmusQueryResponse {
14    track_row: String,
15    player_settings_row: String,
16}
17
18impl FromStr for CmusQueryResponse {
19    type Err = String;
20
21    #[inline]
22    fn from_str(s: &str) -> Result<Self, Self::Err> {
23        #[cfg(feature = "debug")]
24        info!("Parsing cmus response from string: {}", s);
25
26        let sep_index = s.find("set ").ok_or("Corrupted cmus response")?;
27
28        Ok(Self {
29            track_row: s[..sep_index].to_string(),
30            player_settings_row: s[sep_index..].to_string(),
31        })
32    }
33}
34
35impl CmusQueryResponse {
36    /// Actually process and parse the track info, from the cmus response.
37    #[inline(always)]
38    pub fn track(&self) -> Result<Track, CmusError> {
39        Track::from_str(&self.track_row)
40    }
41
42    /// Actually process and parse the player settings, from the cmus response.
43    #[inline(always)]
44    pub fn player_settings(&self) -> Result<PlayerSettings, CmusError> {
45        PlayerSettings::from_str(&self.player_settings_row)
46    }
47
48    /// Compare this response with another one, and return the events that happened.
49    pub fn events(&self, other: &Self) -> Result<Vec<CmusEvent>, CmusError> {
50        #[cfg(feature = "debug")]
51        info!("Comparing cmus responses: {:?} and {:?}", self, other);
52
53        if self.track_row.is_empty() || self.player_settings_row.is_empty() {
54            #[cfg(feature = "debug")]
55            info!("Cmus response is empty, returning empty events");
56            return Ok(Vec::new());
57        }
58
59        let mut events = Vec::new();
60
61        let track = self.track()?;
62        let other_track = other.track()?;
63
64        let other_player_settings = other.player_settings()?;
65
66        if track != other_track {
67            #[cfg(feature = "debug")]
68            debug!("Track changed: {:?} -> {:?}", other_track, track);
69
70            if track.path != other_track.path {
71                #[cfg(feature = "debug")]
72                debug!("Track changed: {:?} -> {:?}", other_track, track);
73                events.push(CmusEvent::TrackChanged(
74                    other_track,
75                    other_player_settings,
76                ));
77                // We don't need to check for other changes, since the track changed.
78                return Ok(events);
79            } else if track.status != other_track.status {
80                #[cfg(feature = "debug")]
81                debug!(
82                    "Status changed: {:?} -> {:?}",
83                    other_track.status, track.status
84                );
85                events.push(CmusEvent::StatusChanged(
86                    other_track.clone(),
87                    other_player_settings.clone(),
88                ));
89            } else if track.position != other_track.position {
90                #[cfg(feature = "debug")]
91                debug!(
92                    "Position changed: {:?} -> {:?}",
93                    other_track.position, track.position
94                );
95                events.push(CmusEvent::PositionChanged(
96                    track,
97                    other_player_settings.clone(),
98                ));
99            }
100        }
101
102        let player_settings = self.player_settings()?;
103
104        if player_settings != other_player_settings {
105            #[cfg(feature = "debug")]
106            debug!(
107                "Player settings changed: {:?} -> {:?}",
108                other_player_settings, player_settings
109            );
110
111            if player_settings.shuffle != other_player_settings.shuffle {
112                #[cfg(feature = "debug")]
113                debug!(
114                    "Shuffle changed: {:?} -> {:?}",
115                    other_player_settings.shuffle, player_settings.shuffle
116                );
117
118                events.push(CmusEvent::ShuffleChanged(
119                    other_track.clone(),
120                    other_player_settings.clone(),
121                ));
122            }
123
124            if player_settings.repeat != other_player_settings.repeat {
125                #[cfg(feature = "debug")]
126                debug!(
127                    "Repeat changed: {:?} -> {:?}",
128                    other_player_settings.repeat, player_settings.repeat
129                );
130
131                events.push(CmusEvent::RepeatChanged(
132                    other_track.clone(),
133                    other_player_settings.clone(),
134                ));
135            }
136
137            if player_settings.aaa_mode != other_player_settings.aaa_mode {
138                #[cfg(feature = "debug")]
139                debug!(
140                    "AAA mode changed: {:?} -> {:?}",
141                    other_player_settings.aaa_mode, player_settings.aaa_mode
142                );
143
144                events.push(CmusEvent::AAAModeChanged(
145                    other_track.clone(),
146                    other_player_settings.clone(),
147                ));
148            }
149
150            if player_settings.volume != other_player_settings.volume {
151                #[cfg(feature = "debug")]
152                debug!(
153                    "Volume changed: {:?} -> {:?}",
154                    other_player_settings.volume, player_settings.volume
155                );
156
157                events.push(CmusEvent::VolumeChanged(other_track, other_player_settings));
158            }
159        }
160
161        #[cfg(feature = "debug")]
162        info!("Returning events: {:?}", events);
163
164        Ok(events)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::cmus::player_settings::{AAAMode, Shuffle};
172    use crate::cmus::TrackStatus;
173    use test_context::{test_context, TestContext};
174
175    #[test]
176    fn test_parse_query_from_str() {
177        let row = include_str!("../../tests/samples/row/cmus-remote-output-row.txt");
178        let query = CmusQueryResponse::from_str(row);
179
180        assert!(query.is_ok());
181        let query = query.unwrap();
182
183        assert_eq!(
184            query.track_row,
185            include_str!("../../tests/samples/row/cmus-remote-output-track-row.txt")
186        );
187        assert_eq!(
188            query.player_settings_row,
189            include_str!("../../tests/samples/row/cmus-remote-output-player-row.txt")
190        );
191    }
192
193    struct Context {
194        query: CmusQueryResponse,
195    }
196
197    impl TestContext for Context {
198        fn setup() -> Self {
199            let row = include_str!("../../tests/samples/row/cmus-remote-output-row.txt");
200            let query = CmusQueryResponse::from_str(row).unwrap();
201
202            Self { query }
203        }
204    }
205
206    #[test_context(Context)]
207    #[test]
208    fn test_actually_parse_the_track_info(ctx: &Context) {
209        let track = ctx.query.track();
210
211        assert!(track.is_ok());
212        let track = track.unwrap();
213
214        assert_eq!(
215            track.path,
216            "/mnt/Data/Music/FLAC/Taylor Swift/Taylor Swift - Speak Now/12 - Haunted.mp3"
217        );
218        assert_eq!(track.status, TrackStatus::Playing);
219        assert_eq!(track.position, 34);
220        assert_eq!(track.duration, 242);
221        let metadata = track.metadata;
222        assert_eq!(metadata.get("artist"), Some("Taylor Swift"));
223        assert_eq!(metadata.get("album"), Some("Speak Now"));
224        assert_eq!(metadata.get("title"), Some("Haunted"));
225        assert_eq!(metadata.get("date"), Some("2010"));
226        assert_eq!(metadata.get("genre"), Some("Pop"));
227        assert_eq!(metadata.get("discnumber"), Some("1"));
228        assert_eq!(metadata.get("tracknumber"), Some("12"));
229        assert_eq!(metadata.get("albumartist"), Some("Taylor Swift"));
230        assert_eq!(metadata.get("replaygain_track_gain"), Some("-11.3 dB"));
231        assert_eq!(metadata.get("composer"), Some("Taylor Swift"));
232        assert_eq!(metadata.get("label"), Some("Big Machine Records, LLC"));
233        assert_eq!(metadata.get("publisher"), Some("Big Machine Records, LLC"));
234        assert_eq!(metadata.get("bpm"), Some("162"));
235        assert_eq!(metadata.get("comment"), None);
236    }
237
238    #[test_context(Context)]
239    #[test]
240    fn test_actually_parse_the_player_settings(ctx: &Context) {
241        let player_settings = ctx.query.player_settings();
242
243        assert!(player_settings.is_ok());
244        let player_settings = player_settings.unwrap();
245
246        assert_eq!(player_settings.aaa_mode, AAAMode::All);
247        assert!(player_settings.repeat);
248        assert!(!player_settings.repeat_current);
249        assert_eq!(player_settings.shuffle, Shuffle::Off);
250        assert_eq!(player_settings.volume.left, 17);
251        assert_eq!(player_settings.volume.right, 17);
252    }
253}