use serde::de; use std::collections::HashMap; mod error; use error::{Error, MpdResult}; use itertools::Itertools; mod structs; pub use structs::{File, Position, State, Stats, Status, Track, UnitResponse}; // some unprintable character to separate repeated keys const SEPARATOR: char = '\x02'; /// Parse an interator of string slices into `T`, /// returning Ok(T) if the data could be deserialized and the MPD response ended with `OK` or /// `Error` if the deserialization failed or MPD sent an error message. /// /// ``` /// # use mparsed::{Track, parse_response}; /// # use std::time::Duration; /// let response = "file: 01 Track.flac /// Last-Modified: 2018-03-07T13:11:43Z /// duration: 123.45 /// Pos: 1 /// Id: 2 /// OK"; /// let parsed: Track = parse_response(response.lines()).unwrap(); /// /// assert_eq!(parsed.file, String::from("01 Track.flac")); /// assert_eq!(parsed.duration, Some(Duration::from_secs_f64(123.45))); /// ``` /// /// For responses that contain multiple instances of a struct (like playlists), see /// [`parse_response_vec`]. /// /// [`parse_response_vec`]: fn.parse_response_vec.html pub fn parse_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()) } /// Parse an iterator of string slices into a vector of `T`, splitting at any occurence of `first_key`. /// /// One possible use for this is the `playlistinfo` command which returns all items in the current /// playlist, where the `file: ` key denotes the start of a new item. /// /// ## Multiple values for `first_key` /// In some cases, like the `listfiles` command, there are multiple possible values for `first_key`, /// so a vector can be specified. /// ``` /// # use mparsed::{parse_response_vec, File}; /// let response = "file: A track.flac /// size: 123456 /// Last-Modified: 2019-12-17T08:51:37Z /// directory: A directory /// Last-Modified: 2015-01-30T14:53:03Z /// OK"; /// let files: Vec = parse_response_vec(response.lines(), &vec!["file: ", "directory: "]).unwrap(); /// /// assert_eq!(files.len(), 2); /// assert_eq!(files[0].name, String::from("A track.flac")); /// assert!(files[1].is_directory()); /// ``` pub fn parse_response_vec<'a, I: Iterator, T: de::DeserializeOwned>(input: I, first_keys: &[&str]) -> MpdResult> { input .peekable() .batching(|it| { let mut v = match it.next() { // if the response is empty or we’ve reached the end, // we receive 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 first key (e.g. `file:`) line which we still need for the next track. while let Some(l) = it.peek() { if first_keys.iter().any(|s| l.starts_with(s)) { return Some(v); } if l.starts_with("OK") { return Some(v); } v.push(it.next().unwrap()); } None }) .map(|b| parse_response(b.into_iter())) .collect() } /// Parse the `playlist` command as a vector of filenames. /// /// The playlist index of each item is *not* included because, if needed, /// it can easily be added with `.enumerate()`. /// /// Note: The MPD protocol documentation suggests using `playlistinfo` instead, /// which returns a superset of this commands output, /// but this isn’t deprecated, so if you only need the filenames, it should be all you need. pub fn parse_playlist<'a, I: Iterator>(input: I) -> MpdResult> { input // `iter.scan((), |(), item| predicate(item))` is equivalent to map_while(predicate), // but the latter isn’t stabilized (yet). .scan((), |(), s| match s.splitn(2, ' ').next_tuple() { Some(("OK", _)) | None => None, // Covers OK with message and without Some(("ACK", err)) => Some(Err(Error::from_str(err))), Some((_, filename)) => Some(Ok(filename)), }) .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 = parse_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 = parse_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 = parse_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_playlistinfo_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 = parse_response_vec(input_str.lines(), &vec!["file: "]); 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 = parse_response_vec("OK".lines(), &vec!["file: "]); assert_eq!(queue, Ok(Vec::::new())); } #[test] fn de_stats_test() { let input_str = "uptime: 23691 playtime: 11288 artists: 2841 albums: 2455 songs: 40322 db_playtime: 11620284 db_update: 1588433046 OK"; let s: Stats = parse_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, } ); } #[test] fn de_unit_response_test() { let success = "OK"; let r: Result = parse_response(success.lines()); assert_eq!(r, Ok(UnitResponse {})); let failure = r#"ACK [2@0] {consume} wrong number of arguments for "consume""#; let r: Result = parse_response(failure.lines()); assert_eq!( r, Err(error::Error::from_str(r#"[2@0] {consume} wrong number of arguments for "consume""#)) ); } #[test] fn parse_playlist_test() { let input = "0:file: Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/01 Opening Screen.flac 1:file: Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/02 Wolfstack Lights.flac 2:file: Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/03 Submergio Viol.flac 3:file: Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/08 Dark Sailing.flac 4:file: Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/17 The Sea Does Not Forgive.flac 5:file: Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/18 Hope Is an Anchor.flac OK"; match parse_playlist(input.lines()) { Err(_) => panic!("Should not be an error"), Ok(tracks) => assert_eq!( tracks, vec![ "Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/01 Opening Screen.flac", "Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/02 Wolfstack Lights.flac", "Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/03 Submergio Viol.flac", "Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/08 Dark Sailing.flac", "Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/17 The Sea Does Not Forgive.flac", "Brent Barkman & Maribeth Solomon/Sunless Sea (2015)/18 Hope Is an Anchor.flac", ] ), } let input = "ACK [] {playlistinfo} something went wrong"; match parse_playlist(input.lines()) { Ok(_) => panic!("Should have failed"), Err(e) => assert_eq!(e.message, "[] {playlistinfo} something went wrong"), } } #[test] fn file_list_test() { let input = "file: 11 奏(かなで)(original スキマスイッチ).flac size: 18948052 Last-Modified: 2019-12-17T08:51:37Z directory: Scans Last-Modified: 2015-01-30T14:53:03Z file: 15 風ハ旅スル(スマートフォンゲーム「風パズル 黒猫と白猫の夢見た世界」テーマ曲).flac size: 13058417 Last-Modified: 2019-12-17T08:51:41Z OK"; let parsed: Vec = parse_response_vec(input.lines(), &vec!["file: ", "directory: "]).unwrap(); assert_eq!( parsed, vec![ File { name: "11 奏(かなで)(original スキマスイッチ).flac".into(), last_modified: DateTime::parse_from_rfc3339("2019-12-17T08:51:37Z").unwrap(), size: 18948052 }, File { name: "Scans".into(), last_modified: DateTime::parse_from_rfc3339("2015-01-30T14:53:03Z").unwrap(), size: -1 }, File { name: "15 風ハ旅スル(スマートフォンゲーム「風パズル 黒猫と白猫の夢見た世界」テーマ曲).flac".into(), last_modified: DateTime::parse_from_rfc3339("2019-12-17T08:51:41Z").unwrap(), size: 13058417 }, ] ); assert_eq!( parsed.iter().map(|f| f.is_directory()).collect::>(), vec![false, true, false], ); } }