Compare commits

...

5 Commits

4 changed files with 49 additions and 15 deletions

2
Cargo.lock generated
View File

@ -50,7 +50,7 @@ checksum = "bd7d4bd64732af4bf3a67f367c27df8520ad7e230c5817b8ff485864d80242b9"
[[package]]
name = "mparsed"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"chrono",
"envy",

View File

@ -2,6 +2,8 @@
A serde parser for MPD responses. Includes mpd structs.
This project is on [crates.io](https://crates.io/crates/mparsed).
## Why?
Because there are lots of mpd client libraries for Rust,
but most (maybe all?) of them write the same awful deserialization code
@ -18,6 +20,15 @@ match key {
And I figured just having a small crate that has all the types and does the serde magic for you would be nice for other people as well.
Now you can simply:
```rs
let raw_response: Vec<String> = my_mpd_client.currentsong(); // <- this one is from your own code
let track: Track = mparsed::deserialize_response(raw_response.iter());
```
No more hand-written deserialization logic.
Oh, and it’s a good learning opportunity for me.
Serde seemed like a library I should learn more about.

View File

@ -58,6 +58,7 @@ pub fn parse_response<'a, I: Iterator<Item = &'a str>, T: de::DeserializeOwned>(
}
/// 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.
///
@ -106,9 +107,14 @@ pub fn parse_response_vec<'a, I: Iterator<Item = &'a str>, T: de::DeserializeOwn
.collect()
}
/// Parse the `playlist` command, a list of key-value pairs, as a vector of filenames.
/// 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),
@ -433,7 +439,7 @@ OK";
File {
name: "Scans".into(),
last_modified: DateTime::parse_from_rfc3339("2015-01-30T14:53:03Z").unwrap(),
size: 0
size: -1
},
File {
name: "15 風ハ旅スル(スマートフォンゲーム「風パズル 黒猫と白猫の夢見た世界」テーマ曲).flac".into(),

View File

@ -1,10 +1,11 @@
use chrono::prelude::*;
use helpers::*;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::{fmt, time::Duration};
/// All information about a track. This is returned by the `currentsong` or `queue` commands.
#[derive(Deserialize, Clone, Debug, Default, PartialEq)]
/// All information about a track. This is returned by the `currentsong`, `queue`, or
/// `playlistinfo` commands.
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(default)]
pub struct Track {
pub file: String,
@ -49,14 +50,15 @@ pub struct Track {
/// An empty struct that can be used as the response data for commands that only ever return `OK`
/// or an error message, e.g. `consume` or other boolean toggles.
#[derive(Deserialize, PartialEq, Debug)]
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct UnitResponse {}
/// The position of an item in a list with an optional total length.
///
/// Can be used for e.g. track number,
/// where `Position { 3, 12 }` represents track 3 of a CD with 12 tracks,
/// or disc numbers in a multi-disc set.
#[derive(Deserialize, Clone, Debug, Default, PartialEq)]
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
pub struct Position {
pub item_position: u16,
pub total_items: Option<u16>,
@ -78,7 +80,7 @@ impl fmt::Display for Position {
/// i.e. on a server with no pulse or alsa output configured.
/// Output via httpd might be used instead
/// but will result in empty `volume` and `audio` fields.
#[derive(Deserialize, Clone, Debug, Default, PartialEq)]
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(default)]
pub struct Status {
pub volume: Option<u8>,
@ -117,23 +119,38 @@ pub struct Status {
pub error: Option<String>,
}
#[derive(Deserialize, Clone, Debug, PartialEq)]
/// An object in the file system, as returned by the `listfiles` command.
///
/// For directories, the `size` will be `-1`, and [`is_directory`] is provided to check that.
///
/// [`is_directory`]: #method.is_directory
///
/// Properly parsing `listfiles` into a Vector of some enum with variants for file and directory
/// would make the deserialization code a lot more complicated, so I’m not interested in doing that
/// at this point in time.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct File {
#[serde(alias = "directory", rename = "file")]
pub name: String,
#[serde(rename = "last-modified")]
pub last_modified: DateTime<FixedOffset>,
#[serde(default)]
pub size: usize,
#[serde(default = "minus_one")]
pub size: i64,
}
fn minus_one() -> i64 {
-1
}
impl File {
/// Returns true if this file is a directory.
/// Internally, this just checks if `size == -1`.
pub fn is_directory(&self) -> bool {
self.size == 0
self.size == -1
}
}
#[derive(Deserialize, Clone, Debug, PartialEq)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum State {
Stop,
Play,
@ -148,7 +165,7 @@ impl Default for State {
}
/// Database statistics as returned by the `stats` command.
#[derive(Deserialize, Clone, Debug, Default, PartialEq)]
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
#[serde(default)]
pub struct Stats {
pub artists: u32,