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)]
69pub 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 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 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 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 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}