mparsed/src/lib.rs

457 lines
15 KiB
Rust

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<Item = &'a str>, T: de::DeserializeOwned>(input: I) -> MpdResult<T> {
let mut map: HashMap<String, String> = 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<File> = 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<Item = &'a str>, T: de::DeserializeOwned>(input: I, first_keys: &[&str]) -> MpdResult<Vec<T>> {
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<Item = &'a str>>(input: I) -> MpdResult<Vec<&'a str>> {
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::<Track>::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<UnitResponse, _> = 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<UnitResponse, _> = 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<File> = 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<_>>(),
vec![false, true, false],
);
}
}