diff --git a/Cargo.lock b/Cargo.lock index 2174157..0270eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,14 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +dependencies = [ + "memchr", +] + [[package]] name = "autocfg" version = "1.0.0" @@ -42,12 +51,24 @@ dependencies = [ "either", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd7d4bd64732af4bf3a67f367c27df8520ad7e230c5817b8ff485864d80242b9" +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + [[package]] name = "mparsed" version = "0.1.0" @@ -55,6 +76,7 @@ dependencies = [ "chrono", "envy", "itertools", + "regex", "serde", ] @@ -95,6 +117,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" + [[package]] name = "serde" version = "1.0.114" @@ -126,6 +166,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + [[package]] name = "time" version = "0.1.43" diff --git a/Cargo.toml b/Cargo.toml index 9ccbf9b..555ee9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,5 +14,6 @@ serde = { version = "1.0.114", features = ["derive"] } itertools = "0.9.0" envy = "0.4.1" chrono = { version = "0.4.13", features = ["serde"] } +regex = "1.3.9" [lib] diff --git a/src/lib.rs b/src/lib.rs index 00a2e69..f0fc316 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,12 +4,37 @@ mod error; use error::{Error, MpdResult}; use itertools::Itertools; mod structs; -pub use structs::{Position, State, Stats, Status, Track, UnitResponse}; +// TODO: If std::str::pattern::Pattern ever gets stabilized, +// use that instead of depending on the Regex crate +use regex::Regex; +pub use structs::{File, Position, State, Stats, Status, Track, UnitResponse}; -/// some unprintable character to separate repeated keys +// some unprintable character to separate repeated keys const SEPARATOR: char = '\x02'; -/// Parse an interator of string slices into `T`. +/// 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 { @@ -38,7 +63,25 @@ pub fn parse_response<'a, I: Iterator, T: de::DeserializeOwned>( /// Parse an iterator of string slices into a vector of `T`, splitting at `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. -pub fn parse_response_vec<'a, 'b, I: Iterator, T: de::DeserializeOwned>(input: I, first_key: &'b str) -> MpdResult> { +/// +/// # `first_key` as Regex +/// In some cases, like the `listfiles` command, there are multiple options for `first_key`, so a +/// proper regex must be specified. +/// ``` +/// # use regex::Regex; +/// # 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(), Regex::new("^(file|directory): ").unwrap()).unwrap(); +/// +/// assert_eq!(files[0].name, String::from("A track.flac")); +/// assert!(files[1].is_directory()); +/// ``` +pub fn parse_response_vec<'a, 'b, I: Iterator, T: de::DeserializeOwned>(input: I, first_key: Regex) -> MpdResult> { input .peekable() .batching(|it| { @@ -52,7 +95,7 @@ pub fn parse_response_vec<'a, 'b, I: Iterator, T: de::Deserializ }; // 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 l.starts_with(first_key) { + if first_key.is_match(l) { return Some(v); } if l.starts_with("OK") { @@ -260,7 +303,7 @@ duration: 50.155 Pos: 1002 Id: 6367 OK"; - let queue = parse_response_vec(input_str.lines(), "file:"); + let queue = parse_response_vec(input_str.lines(), Regex::new("^file:").unwrap()); let first_track = Track { file: "137 A New World.mp3".into(), title: Some("A New World".into()), @@ -295,7 +338,7 @@ OK"; ]) ); - let queue = parse_response_vec("OK".lines(), "file:"); + let queue = parse_response_vec("OK".lines(), Regex::new("^file:").unwrap()); assert_eq!(queue, Ok(Vec::::new())); } @@ -365,8 +408,46 @@ OK"; 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") + 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(), Regex::new("^(file:|directory:)").unwrap()).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: 0 + }, + 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], + ); + } } diff --git a/src/structs.rs b/src/structs.rs index 41a7b32..f5fb8e3 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -117,6 +117,22 @@ pub struct Status { pub error: Option, } +#[derive(Deserialize, Clone, Debug, PartialEq)] +pub struct File { + #[serde(alias = "directory", rename = "file")] + pub name: String, + #[serde(rename = "last-modified")] + pub last_modified: DateTime, + #[serde(default)] + pub size: usize, +} + +impl File { + pub fn is_directory(&self) -> bool { + self.size == 0 + } +} + #[derive(Deserialize, Clone, Debug, PartialEq)] pub enum State { Stop,