2020-06-21 14:20:35 +02:00
use serde ::de ;
2020-06-20 22:50:33 +02:00
use std ::collections ::HashMap ;
mod error ;
use error ::{ Error , MpdResult } ;
2020-06-21 15:31:08 +02:00
use itertools ::Itertools ;
2020-06-20 22:50:33 +02:00
mod structs ;
2020-07-24 14:46:51 +02:00
pub use structs ::{ File , Position , State , Stats , Status , Track , UnitResponse } ;
2020-06-20 22:50:33 +02:00
2020-07-24 14:46:51 +02:00
// some unprintable character to separate repeated keys
2020-06-20 22:50:33 +02:00
const SEPARATOR : char = '\x02' ;
2020-07-24 14:46:51 +02:00
/// 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
2020-06-21 23:37:19 +02:00
pub fn parse_response < ' a , I : Iterator < Item = & ' a str > , T : de ::DeserializeOwned > ( input : I ) -> MpdResult < T > {
2020-06-20 22:50:33 +02:00
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 ( ) ) ) ;
}
2020-06-21 14:20:35 +02:00
2020-06-21 23:11:41 +02:00
match line . splitn ( 2 , " : " ) . next_tuple ( ) {
Some ( ( k , v ) ) = > {
2020-06-20 22:50:33 +02:00
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
2020-06-21 14:20:35 +02:00
Ok ( envy ::from_iter ( map ) . unwrap ( ) )
2020-06-20 22:50:33 +02:00
}
2020-07-24 15:00:20 +02:00
/// Parse an iterator of string slices into a vector of `T`, splitting at any occurence of `first_key`.
2020-07-24 17:19:43 +02:00
///
2020-07-23 21:03:13 +02:00
/// 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.
2020-07-24 14:46:51 +02:00
///
2020-07-24 15:00:20 +02:00
/// ## 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.
2020-07-24 14:46:51 +02:00
/// ```
/// # 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";
2020-07-24 15:00:20 +02:00
/// let files: Vec<File> = parse_response_vec(response.lines(), &vec!["file: ", "directory: "]).unwrap();
2020-07-24 14:46:51 +02:00
///
2020-07-24 15:00:20 +02:00
/// assert_eq!(files.len(), 2);
2020-07-24 14:46:51 +02:00
/// assert_eq!(files[0].name, String::from("A track.flac"));
/// assert!(files[1].is_directory());
/// ```
2020-07-24 15:00:20 +02:00
pub fn parse_response_vec < ' a , I : Iterator < Item = & ' a str > , T : de ::DeserializeOwned > ( input : I , first_keys : & [ & str ] ) -> MpdResult < Vec < T > > {
2020-06-21 15:31:08 +02:00
input
. peekable ( )
. batching ( | it | {
let mut v = match it . next ( ) {
2020-07-23 21:03:13 +02:00
// if the response is empty or we’ve reached the end,
// we receive only the `OK` line.
2020-06-21 15:31:08 +02:00
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 ,
} ;
2020-07-23 21:03:13 +02:00
// Only peek here in case we encounter the first key (e.g. `file:`) line which we still need for the next track.
2020-06-21 15:31:08 +02:00
while let Some ( l ) = it . peek ( ) {
2020-07-24 15:00:20 +02:00
if first_keys . iter ( ) . any ( | s | l . starts_with ( s ) ) {
2020-06-21 15:31:08 +02:00
return Some ( v ) ;
}
if l . starts_with ( " OK " ) {
return Some ( v ) ;
}
v . push ( it . next ( ) . unwrap ( ) ) ;
}
None
} )
2020-06-21 23:37:19 +02:00
. map ( | b | parse_response ( b . into_iter ( ) ) )
2020-06-21 15:31:08 +02:00
. collect ( )
}
2020-07-24 17:19:43 +02:00
/// Parse the `playlist` command as a vector of filenames.
///
2020-07-23 21:03:13 +02:00
/// The playlist index of each item is *not* included because, if needed,
/// it can easily be added with `.enumerate()`.
2020-07-24 17:16:03 +02:00
///
/// 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.
2020-07-23 21:03:13 +02:00
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 ( )
}
2020-06-20 22:50:33 +02:00
#[ cfg(test) ]
mod tests {
use super ::* ;
2020-06-21 01:00:29 +02:00
use chrono ::DateTime ;
2020-06-21 14:50:25 +02:00
use std ::time ::Duration ;
2020-06-20 22:50:33 +02:00
#[ test ]
fn track_de_test ( ) {
let input_str = " file: American Pie.flac
Last - Modified : 2019 - 12 - 16 T21 :38 :54 Z
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 " ;
2020-06-21 23:37:19 +02:00
let t : Track = parse_response ( input_str . lines ( ) ) . unwrap ( ) ;
2020-06-20 22:50:33 +02:00
assert_eq! (
t ,
Track {
file : " American Pie.flac " . to_string ( ) ,
2020-06-21 01:00:29 +02:00
last_modified : Some ( DateTime ::parse_from_rfc3339 ( " 2019-12-16T21:38:54Z " ) . unwrap ( ) ) ,
2020-06-20 22:50:33 +02:00
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 ( ) ) ,
2020-06-20 23:27:00 +02:00
track : Some ( Position {
item_position : 1 ,
2020-07-23 21:03:13 +02:00
total_items : None ,
2020-06-20 23:27:00 +02:00
} ) ,
2020-06-21 14:50:25 +02:00
duration : Some ( Duration ::from_secs_f64 ( 512.380 ) ) ,
2020-06-20 22:50:33 +02:00
pos : 1367 ,
id : 1368 ,
.. Track ::default ( )
}
) ;
}
2020-06-20 23:27:00 +02:00
#[ 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 - 11 T09 :01 :54 Z
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 - 474 e - b64a - 803 b0364fa75
MUSICBRAINZ_ALBUMID : 688 d9252 - f897 - 4 ab6 - 879 d - 5 cb83bb71087
MUSICBRAINZ_ARTISTID : b972f589 - fb0e - 474 e - b64a - 803 b0364fa75
MUSICBRAINZ_RELEASETRACKID : 54 a2632f - fa98 - 3713 - bd75 - d7effc03d0b1
MUSICBRAINZ_TRACKID : 2 dd10dd8 - e8de - 4479 - a092 - 9 a04c2760fd6
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 " #;
2020-06-21 23:37:19 +02:00
let t : Track = parse_response ( input_str . lines ( ) ) . unwrap ( ) ;
2020-06-20 23:27:00 +02:00
assert_eq! (
t ,
Track {
file : " 06 Symphonie No. 41 en ut majeur, K. 551 _Jupiter_ I. Allegro Vivace.flac " . to_string ( ) ,
2020-06-21 01:00:29 +02:00
last_modified : Some ( DateTime ::parse_from_rfc3339 ( " 2018-11-11T09:01:54Z " ) . unwrap ( ) ) ,
2020-06-20 23:27:00 +02:00
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 ,
2020-07-23 21:03:13 +02:00
total_items : Some ( 1 ) ,
2020-06-20 23:27:00 +02:00
} ) ,
track : Some ( Position {
item_position : 6 ,
2020-07-23 21:03:13 +02:00
total_items : Some ( 6 ) ,
2020-06-20 23:27:00 +02:00
} ) ,
2020-06-21 14:50:25 +02:00
duration : Some ( Duration ::from_secs_f64 ( 682.840 ) ) ,
2020-06-20 23:27:00 +02:00
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 ( )
}
) ;
}
2020-06-21 14:50:25 +02:00
#[ 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 " ;
2020-06-21 23:37:19 +02:00
let s : Status = parse_response ( input_str . lines ( ) ) . unwrap ( ) ;
2020-06-21 14:50:25 +02:00
assert_eq! (
s ,
Status {
volume : Some ( 74 ) ,
random : true ,
playlist : 6 ,
playlistlength : 5364 ,
mixrampdb : 0.0 ,
2020-06-21 23:03:42 +02:00
state : State ::Play ,
2020-06-21 14:50:25 +02:00
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 ( )
}
) ;
}
2020-06-21 15:31:08 +02:00
#[ test ]
2020-07-23 21:03:13 +02:00
fn de_playlistinfo_test ( ) {
2020-06-21 15:31:08 +02:00
let input_str = " file: 137 A New World.mp3
Last - Modified : 2018 - 03 - 07 T13 :11 :43 Z
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 - 07 T13 :11 :43 Z
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 - 07 T13 :11 :43 Z
Artist : Arata Iiyoshi
Title : A Message On the Wind
Album : Pokemon Mystery Dungeon : Explorers of Sky
duration : 50.155
Pos : 1002
Id : 6367
OK " ;
2020-07-24 15:00:20 +02:00
let queue = parse_response_vec ( input_str . lines ( ) , & vec! [ " file: " ] ) ;
2020-06-21 15:31:08 +02:00
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
} ,
] )
) ;
2020-07-24 15:00:20 +02:00
let queue = parse_response_vec ( " OK " . lines ( ) , & vec! [ " file: " ] ) ;
2020-06-21 23:37:19 +02:00
assert_eq! ( queue , Ok ( Vec ::< Track > ::new ( ) ) ) ;
2020-06-21 15:31:08 +02:00
}
2020-06-21 19:12:36 +02:00
#[ test ]
fn de_stats_test ( ) {
let input_str = " uptime: 23691
playtime : 11288
artists : 2841
albums : 2455
songs : 40322
db_playtime : 11620284
2020-06-21 23:19:12 +02:00
db_update : 1588433046
OK " ;
2020-06-21 23:37:19 +02:00
let s : Stats = parse_response ( input_str . lines ( ) ) . unwrap ( ) ;
2020-06-21 19:12:36 +02:00
assert_eq! (
s ,
Stats {
2020-07-23 21:03:13 +02:00
uptime : Duration ::from_secs ( 23691 ) ,
playtime : Duration ::from_secs ( 11288 ) ,
artists : 2841 ,
albums : 2455 ,
songs : 40322 ,
2020-06-21 19:12:36 +02:00
db_playtime : Duration ::from_secs ( 11620284 ) ,
2020-07-23 21:03:13 +02:00
db_update : 1588433046 ,
2020-06-21 19:12:36 +02:00
}
) ;
}
2020-06-21 23:19:12 +02:00
#[ test ]
fn de_unit_response_test ( ) {
let success = " OK " ;
2020-06-21 23:37:19 +02:00
let r : Result < UnitResponse , _ > = parse_response ( success . lines ( ) ) ;
2020-06-21 23:19:12 +02:00
assert_eq! ( r , Ok ( UnitResponse { } ) ) ;
let failure = r # "ACK [2@0] {consume} wrong number of arguments for "consume""# ;
2020-06-21 23:37:19 +02:00
let r : Result < UnitResponse , _ > = parse_response ( failure . lines ( ) ) ;
2020-06-21 23:19:12 +02:00
assert_eq! (
r ,
Err ( error ::Error ::from_str ( r # "[2@0] {consume} wrong number of arguments for "consume""# ) )
) ;
}
2020-07-23 21:03:13 +02:00
#[ 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
2020-07-23 21:12:44 +02:00
5 :file : Brent Barkman & Maribeth Solomon / Sunless Sea ( 2015 ) / 18 Hope Is an Anchor . flac
OK " ;
2020-07-23 21:03:13 +02:00
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 " ,
]
) ,
}
2020-07-23 21:12:44 +02:00
let input = " ACK [] {playlistinfo} something went wrong " ;
match parse_playlist ( input . lines ( ) ) {
2020-07-24 14:46:51 +02:00
Ok ( _ ) = > panic! ( " Should have failed " ) ,
Err ( e ) = > assert_eq! ( e . message , " [] {playlistinfo} something went wrong " ) ,
2020-07-23 21:12:44 +02:00
}
2020-07-23 21:03:13 +02:00
}
2020-07-24 14:46:51 +02:00
#[ test ]
fn file_list_test ( ) {
let input = " file: 11 奏(かなで)(original スキマスイッチ).flac
size : 18948052
Last - Modified : 2019 - 12 - 17 T08 :51 :37 Z
directory : Scans
Last - Modified : 2015 - 01 - 30 T14 :53 :03 Z
file : 15 風 ハ 旅 ス ル ( ス マ ー ト フ ォ ン ゲ ー ム 「 風 パ ズ ル 黒 猫 と 白 猫 の 夢 見 た 世 界 」 テ ー マ 曲 ) . flac
size : 13058417
Last - Modified : 2019 - 12 - 17 T08 :51 :41 Z
OK " ;
2020-07-24 15:00:20 +02:00
let parsed : Vec < File > = parse_response_vec ( input . lines ( ) , & vec! [ " file: " , " directory: " ] ) . unwrap ( ) ;
2020-07-24 14:46:51 +02:00
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 ( ) ,
2020-07-24 17:16:03 +02:00
size : - 1
2020-07-24 14:46:51 +02:00
} ,
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 ] ,
) ;
}
2020-06-20 22:50:33 +02:00
}