async_mpd/client/resp/
respmap_handlers.rs

1use std::str::FromStr;
2
3use async_net::TcpStream;
4use futures_lite::{io::AsyncBufReadExt, io::BufReader, StreamExt};
5use serde::Serialize;
6
7use crate::client::resp::respmap::RespMap;
8use crate::{DatabaseVersion, Directory, Playlist, State, Stats, Status, Subsystem, Track};
9use std::convert::TryFrom;
10
11impl From<RespMap> for Subsystem {
12    fn from(mut map: RespMap) -> Self {
13        let s: String = map.get("subsystem").unwrap_or_else(|| "other".into());
14
15        match s.as_ref() {
16            "partitions" => Subsystem::Partitions,
17            "player" => Subsystem::Player,
18            "mixer" => Subsystem::Mixer,
19            "options" => Subsystem::Options,
20            "update" => Subsystem::Update,
21            "storedplaylist" => Subsystem::StoredPlaylist,
22            "output" => Subsystem::Output,
23            _ => Subsystem::Other,
24        }
25    }
26}
27
28pub struct ListallResponse {
29    pub files: Vec<String>,
30    pub dirs: Vec<String>,
31    pub playlists: Vec<String>,
32}
33
34impl From<RespMap> for ListallResponse {
35    fn from(mut map: RespMap) -> Self {
36        let files = map.get_vec("file");
37        let dirs = map.get_vec("directory");
38        let playlists = map.get_vec("playlist");
39        ListallResponse {
40            files,
41            dirs,
42            playlists,
43        }
44    }
45}
46
47impl From<RespMap> for DatabaseVersion {
48    fn from(mut map: RespMap) -> Self {
49        let v = map.get_def("updating_db");
50        DatabaseVersion(v)
51    }
52}
53
54impl FromStr for State {
55    type Err = crate::Error;
56
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        let status = match s {
59            "play" => State::Play,
60            "pause" => State::Pause,
61            "stop" => State::Stop,
62            _ => return Err(crate::Error::ValueError { msg: s.into() }),
63        };
64        Ok(status)
65    }
66}
67
68#[derive(Serialize, Debug)]
69/// Response from commands that returns entries with metadata and tags
70pub enum MixedResponse {
71    File(Track),
72    Directory(Directory),
73    Playlist(Playlist),
74}
75
76pub(crate) async fn tracks(stream: &mut BufReader<TcpStream>) -> std::io::Result<Vec<Track>> {
77    Ok(mixed_stream(stream).await?.files)
78}
79
80impl From<RespMap> for Directory {
81    fn from(mut map: RespMap) -> Self {
82        let dir = Directory {
83            path: map.get_def("directory"),
84            last_modified: map.get("Last-Modified"),
85        };
86
87        if !map.is_empty() {
88            log::warn!("Status map not empty: {:?}", map.inner);
89        }
90
91        dir
92    }
93}
94
95impl From<RespMap> for Playlist {
96    fn from(mut map: RespMap) -> Self {
97        let playlist = Playlist {
98            path: map.get_def("playlist"),
99            last_modified: map.get("Last-Modified"),
100        };
101
102        if !map.is_empty() {
103            log::warn!("Status map not empty: {:?}", map.inner);
104        }
105
106        playlist
107    }
108}
109
110pub struct ListallinfoResponse {
111    pub files: Vec<Track>,
112    pub dirs: Vec<Directory>,
113    pub playlist: Vec<Playlist>,
114}
115
116impl TryFrom<RespMap> for MixedResponse {
117    type Error = ();
118
119    fn try_from(map: RespMap) -> Result<Self, Self::Error> {
120        if map.contains_key("directory") {
121            Ok(MixedResponse::Directory(Directory::from(map)))
122        } else if map.contains_key("playlist") {
123            Ok(MixedResponse::Playlist(Playlist::from(map)))
124        } else if map.contains_key("file") {
125            Ok(MixedResponse::File(Track::from(map)))
126        } else {
127            Err(())
128        }
129    }
130}
131
132pub async fn mixed_stream(
133    stream: &mut BufReader<TcpStream>,
134) -> std::io::Result<ListallinfoResponse> {
135    let mut resvec = ListallinfoResponse {
136        files: vec![],
137        dirs: vec![],
138        playlist: vec![],
139    };
140    let mut map = RespMap::new();
141    let mut lines = stream.lines();
142
143    while let Some(line) = lines.next().await {
144        let line = line?;
145        let line = line.trim();
146
147        log::debug!("{}", line);
148
149        if line == "OK" {
150            // We're done
151
152            if let Ok(dtp) = MixedResponse::try_from(map) {
153                match dtp {
154                    MixedResponse::File(t) => resvec.files.push(t),
155                    MixedResponse::Directory(d) => resvec.dirs.push(d),
156                    MixedResponse::Playlist(pl) => resvec.playlist.push(pl),
157                }
158            }
159
160            // Add the previous record to the result vec
161            break;
162        }
163
164        if !map.is_empty()
165            && (line.starts_with("directory:")
166                || line.starts_with("file:")
167                || line.starts_with("playlist:"))
168        {
169            if let Ok(dtp) = MixedResponse::try_from(map) {
170                // Add the previous record to the result vec
171                match dtp {
172                    MixedResponse::File(t) => resvec.files.push(t),
173                    MixedResponse::Directory(d) => resvec.dirs.push(d),
174                    MixedResponse::Playlist(pl) => resvec.playlist.push(pl),
175                }
176            }
177
178            // Open a new record
179            map = RespMap::new();
180        }
181
182        if let Some((k, v)) = line.split_once(": ") {
183            map.insert(k, v);
184        }
185    }
186
187    Ok(resvec)
188}
189
190impl From<RespMap> for Track {
191    fn from(mut map: RespMap) -> Self {
192        let track = Track {
193            file: map.get_def("file"),
194            artist_sort: map.get("ArtistSort"),
195            album_artist: map.get("AlbumArtist"),
196            album_sort: map.get("AlbumSort"),
197            album_artist_sort: map.get("AlbumArtistSort"),
198            performer: map.get_vec("Performer"),
199            genre: map.get("Genre"),
200            title: map.get("Title"),
201            track: map.get("Track"),
202            album: map.get("Album"),
203            artist: map.get("Artist"),
204            pos: map.get("Pos"),
205            id: map.get("Id"),
206            last_modified: map.get("Last-Modified"),
207            original_date: map.get("OriginalDate"),
208            time: map.get("Time"),
209            format: map.get("Format"),
210            duration: map.as_duration_def("duration"),
211            label: map.get("Label"),
212            date: map.get("Date"),
213            disc: map.get("Disc"),
214            musicbraiz_trackid: map.get("MUSICBRAINZ_TRACKID"),
215            musicbrainz_albumid: map.get("MUSICBRAINZ_ALBUMID"),
216            musicbrainz_albumartistid: map.get("MUSICBRAINZ_ALBUMARTISTID"),
217            musicbrainz_artistid: map.get("MUSICBRAINZ_ARTISTID"),
218            musicbraiz_releasetrackid: map.get("MUSICBRAINZ_RELEASETRACKID"),
219            musicbraiz_workid: map.get("MUSICBRAINZ_WORKID"),
220            composer: map.get_vec("Composer"),
221        };
222
223        if !map.is_empty() {
224            log::warn!("Track map not empty: {:?}", map.inner);
225        }
226
227        track
228    }
229}
230
231impl From<RespMap> for Status {
232    fn from(mut map: RespMap) -> Self {
233        let status = Status {
234            partition: map.get("partition"),
235            volume: map.get("volume"),
236            repeat: map.as_bool("repeat"),
237            random: map.as_bool("random"),
238            single: map.get_def("single"),
239            consume: map.as_bool("consume"),
240            playlist: map.get_def("playlist"),
241            playlistlength: map.get_def("playlistlength"),
242            song: map.get("song"),
243            songid: map.get("songid"),
244            nextsong: map.get("nextsong"),
245            nextsongid: map.get("nextsongid"),
246            time: map.get("time"),
247            elapsed: map.as_duration("elapsed"),
248            duration: map.as_duration("duration"),
249            mixrampdb: map.get_def("mixrampdb"),
250            mixrampdelay: map.get("mixrampdelay"),
251            state: map.get_def("state"),
252            bitrate: map.get("bitrate"),
253            xfade: map.get("xfade"),
254            audio: map.get("audio"),
255            updating_db: map.get("updating_db"),
256            error: map.get("error"),
257        };
258
259        if !map.is_empty() {
260            log::warn!("Status map not empty: {:?}", map.inner);
261        }
262
263        status
264    }
265}
266
267impl From<RespMap> for Stats {
268    fn from(mut map: RespMap) -> Self {
269        let stats = Stats {
270            uptime: map.as_duration_def("uptime"),
271            playtime: map.as_duration_def("playtime"),
272            artists: map.get_def("artists"),
273            albums: map.get_def("albums"),
274            songs: map.get_def("songs"),
275            db_playtime: map.as_duration_def("db_playtime"),
276            db_update: map.get_def("db_update"),
277        };
278
279        if !map.is_empty() {
280            log::warn!("Status map not empty: {:?}", map.inner);
281        }
282        stats
283    }
284}
285
286#[cfg(test)]
287mod test {
288    use crate::client::resp::respmap::RespMap;
289    use crate::{State, Status};
290    use std::time::Duration;
291
292    #[test]
293    fn parse_status() {
294        let input = r#"\
295volume: 50
296repeat: 1
297random: 1
298single: 0
299consume: 0
300playlist: 2
301playlistlength: 141
302mixrampdb: 0.000000
303state: play
304song: 1
305songid: 2
306time: 149:308
307elapsed: 149.029
308bitrate: 878
309duration: 307.760
310audio: 44100:16:2
311nextsong: 124
312nextsongid: 125
313"#;
314
315        let reference = Status {
316            partition: None,
317            volume: Some(50),
318            repeat: true,
319            random: true,
320            single: "0".into(),
321            consume: false,
322            playlist: 2,
323            playlistlength: 141,
324            song: Some(1),
325            songid: Some(2),
326            nextsong: Some(124),
327            nextsongid: Some(125),
328            time: Some("149:308".into()),
329            elapsed: Some(Duration::from_secs_f64(149.029)),
330            duration: Some(Duration::from_secs_f64(307.76)),
331            mixrampdb: 0.0,
332            mixrampdelay: None,
333            state: State::Play,
334            bitrate: Some(878),
335            xfade: None,
336            audio: Some("44100:16:2".into()),
337            updating_db: None,
338            error: None,
339        };
340
341        let parsed = Status::from(RespMap::from_string(input.into()));
342        assert_eq!(parsed, reference);
343    }
344}