use serde::de; use std::collections::HashMap; mod error; use error::{Error, MpdResult}; use itertools::Itertools; mod structs; pub use structs::{Position, State, Stats, Status, Track}; /// some unprintable character to separate repeated keys const SEPARATOR: char = '\x02'; pub fn deserialize_response<'a, I: Iterator, T: de::DeserializeOwned>(input: I) -> MpdResult { let mut map: HashMap = HashMap::new(); for line in input { if line.starts_with("OK") { break; } else if let Some(message) = line.strip_prefix("ACK") { return Err(Error::from_str(message.trim())); } match line.splitn(2, ": ").next_tuple() { Some((k, v)) => { if let Some(existing) = map.get_mut(k) { existing.push(SEPARATOR); existing.push_str(v); } else { map.insert(k.to_string(), v.to_string()); } } _ => panic!("invalid response line: {:?}", line), } } // Eventually, I’d like to use my own deserializer instead of envy Ok(envy::from_iter(map).unwrap()) } pub fn read_playlist_info<'a, I: Iterator>(input: I) -> MpdResult> { input .peekable() .batching(|it| { let mut v = match it.next() { // if the playlist is empty or we’ve reached the end, // the response is only the `OK` line. Some("OK") => return None, // otherwise this is the file attribute and thus the first key-value of this track Some(s) => vec![s], _ => return None, }; // Only peek here in case we encounter the `file:` line which we still need for the next track. while let Some(l) = it.peek() { if l.starts_with("file:") { return Some(v); } if l.starts_with("OK") { return Some(v); } v.push(it.next().unwrap()); } None }) .map(|b| deserialize_response(b.into_iter())) .collect() } #[cfg(test)] mod tests { use super::*; use chrono::DateTime; use std::time::Duration; #[test] fn track_de_test() { let input_str = "file: American Pie.flac Last-Modified: 2019-12-16T21:38:54Z Album: American Pie Artist: Don McLean Date: 1979 Genre: Rock Title: American Pie Track: 1 Time: 512 duration: 512.380 Pos: 1367 Id: 1368 OK"; let t: Track = deserialize_response(input_str.lines()).unwrap(); assert_eq!( t, Track { file: "American Pie.flac".to_string(), last_modified: Some(DateTime::parse_from_rfc3339("2019-12-16T21:38:54Z").unwrap()), album: Some("American Pie".to_string()), artist: Some("Don McLean".to_string()), date: Some(1979), genre: Some("Rock".to_string()), title: Some("American Pie".to_string()), track: Some(Position { item_position: 1, total_items: None, }), duration: Some(Duration::from_secs_f64(512.380)), pos: 1367, id: 1368, ..Track::default() } ); } #[test] fn repeated_field_test() { let input_str = r#"file: 06 Symphonie No. 41 en ut majeur, K. 551 _Jupiter_ I. Allegro Vivace.flac Last-Modified: 2018-11-11T09:01:54Z Album: Symphonies n°40 & n°41 AlbumArtist: Wolfgang Amadeus Mozart; Royal Philharmonic Orchestra, Jane Glover AlbumArtistSort: Mozart, Wolfgang Amadeus; Royal Philharmonic Orchestra, Glover, Jane Artist: Wolfgang Amadeus Mozart ArtistSort: Mozart, Wolfgang Amadeus Composer: Wolfgang Amadeus Mozart Date: 2005 Disc: 1 Disc: 1 MUSICBRAINZ_ALBUMARTISTID: b972f589-fb0e-474e-b64a-803b0364fa75 MUSICBRAINZ_ALBUMID: 688d9252-f897-4ab6-879d-5cb83bb71087 MUSICBRAINZ_ARTISTID: b972f589-fb0e-474e-b64a-803b0364fa75 MUSICBRAINZ_RELEASETRACKID: 54a2632f-fa98-3713-bd75-d7effc03d0b1 MUSICBRAINZ_TRACKID: 2dd10dd8-e8de-4479-a092-9a04c2760fd6 OriginalDate: 1993 Title: Symphonie No. 41 en ut majeur, K. 551 "Jupiter": I. Allegro Vivace Track: 6 Track: 6 Time: 683 Performer: Royal Philharmonic Orchestra duration: 682.840 Performer: Jane Glover Pos: 3439 Id: 3440 OK"#; let t: Track = deserialize_response(input_str.lines()).unwrap(); assert_eq!( t, Track { file: "06 Symphonie No. 41 en ut majeur, K. 551 _Jupiter_ I. Allegro Vivace.flac".to_string(), last_modified: Some(DateTime::parse_from_rfc3339("2018-11-11T09:01:54Z").unwrap()), album: Some("Symphonies n°40 & n°41".to_string()), artist: Some("Wolfgang Amadeus Mozart".to_string()), artist_sort: Some("Mozart, Wolfgang Amadeus".to_string()), album_artist: Some("Wolfgang Amadeus Mozart; Royal Philharmonic Orchestra, Jane Glover".to_string()), album_artist_sort: Some("Mozart, Wolfgang Amadeus; Royal Philharmonic Orchestra, Glover, Jane".to_string()), composer: Some("Wolfgang Amadeus Mozart".to_string()), date: Some(2005), original_date: Some(1993), title: Some("Symphonie No. 41 en ut majeur, K. 551 \"Jupiter\": I. Allegro Vivace".to_string()), disc: Some(Position { item_position: 1, total_items: Some(1), }), track: Some(Position { item_position: 6, total_items: Some(6), }), duration: Some(Duration::from_secs_f64(682.840)), pos: 3439, id: 3440, performers: vec!["Royal Philharmonic Orchestra".to_string(), "Jane Glover".to_string()], musicbrainz_albumartistid: Some("b972f589-fb0e-474e-b64a-803b0364fa75".to_string()), musicbrainz_albumid: Some("688d9252-f897-4ab6-879d-5cb83bb71087".to_string()), musicbrainz_artistid: Some("b972f589-fb0e-474e-b64a-803b0364fa75".to_string()), musicbrainz_releasetrackid: Some("54a2632f-fa98-3713-bd75-d7effc03d0b1".to_string()), musicbrainz_trackid: Some("2dd10dd8-e8de-4479-a092-9a04c2760fd6".to_string()), ..Track::default() } ); } #[test] fn de_status_test() { let input_str = "volume: 74 repeat: 0 random: 1 single: 0 consume: 0 playlist: 6 playlistlength: 5364 mixrampdb: 0.000000 state: play song: 3833 songid: 3834 time: 70:164 elapsed: 69.642 bitrate: 702 duration: 163.760 audio: 44100:16:2 nextsong: 4036 nextsongid: 4037 OK"; let s: Status = deserialize_response(input_str.lines()).unwrap(); assert_eq!( s, Status { volume: Some(74), random: true, playlist: 6, playlistlength: 5364, mixrampdb: 0.0, state: State::Play, song: Some(3833), songid: Some(3834), elapsed: Some(Duration::from_secs_f64(69.642)), bitrate: Some(702), duration: Some(Duration::from_secs_f64(163.760)), audio: Some(String::from("44100:16:2")), nextsong: Some(4036), nextsongid: Some(4037), ..Status::default() } ); } #[test] fn de_playlist_test() { let input_str = "file: 137 A New World.mp3 Last-Modified: 2018-03-07T13:11:43Z Artist: Arata Iiyoshi Title: A New World Album: Pokemon Mystery Dungeon: Explorers of Sky duration: 225.802 Pos: 1000 Id: 6365 file: 139 Thoughts For Friends.mp3 Last-Modified: 2018-03-07T13:11:43Z Artist: Arata Iiyoshi Title: Thoughts For Friends Album: Pokemon Mystery Dungeon: Explorers of Sky duration: 66.560 Pos: 1001 Id: 6366 file: 140 A Message On the Wind.mp3 Last-Modified: 2018-03-07T13:11:43Z Artist: Arata Iiyoshi Title: A Message On the Wind Album: Pokemon Mystery Dungeon: Explorers of Sky duration: 50.155 Pos: 1002 Id: 6367 OK"; let queue = read_playlist_info(input_str.lines()); let first_track = Track { file: "137 A New World.mp3".into(), title: Some("A New World".into()), artist: Some("Arata Iiyoshi".into()), album: Some("Pokemon Mystery Dungeon: Explorers of Sky".into()), last_modified: Some(DateTime::parse_from_rfc3339("2018-03-07T13:11:43Z").unwrap()), duration: Some(Duration::from_secs_f64(225.802)), pos: 1000, id: 6365, ..Track::default() }; assert_eq!( queue, Ok(vec![ first_track.clone(), Track { file: "139 Thoughts For Friends.mp3".into(), title: Some("Thoughts For Friends".into()), duration: Some(Duration::from_secs_f64(66.56)), pos: 1001, id: 6366, ..first_track.clone() }, Track { file: "140 A Message On the Wind.mp3".into(), title: Some("A Message On the Wind".into()), duration: Some(Duration::from_secs_f64(50.155)), pos: 1002, id: 6367, ..first_track }, ]) ); let queue = read_playlist_info("OK".lines()); assert_eq!(queue, Ok(vec![])); } #[test] fn de_stats_test() { let input_str = "uptime: 23691 playtime: 11288 artists: 2841 albums: 2455 songs: 40322 db_playtime: 11620284 db_update: 1588433046"; let s: Stats = deserialize_response(input_str.lines()).unwrap(); assert_eq!( s, Stats { uptime: Duration::from_secs(23691), playtime: Duration::from_secs(11288), artists: 2841, albums: 2455, songs: 40322, db_playtime: Duration::from_secs(11620284), db_update: 1588433046, } ); } }