Skip to content

Commit 42177f6

Browse files
authored
Merge pull request #74 from htkhiem/main
MPD v0.21+ filter syntax, DSD support & more
2 parents 216cdcd + 81d6508 commit 42177f6

File tree

3 files changed

+154
-24
lines changed

3 files changed

+154
-24
lines changed

src/output.rs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
//! The module describes output
22
3-
use crate::convert::FromMap;
3+
use crate::convert::FromIter;
44
use crate::error::{Error, ProtoError};
5-
use std::collections::BTreeMap;
6-
use std::convert::From;
75

86
/// Sound output
97
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -17,15 +15,51 @@ pub struct Output {
1715
pub name: String,
1816
/// enabled state
1917
pub enabled: bool,
18+
/// Runtime-configurable, plugin-specific attributes, such as "dop" for ALSA
19+
pub attributes: Vec<(String, String)>
2020
}
2121

22-
impl FromMap for Output {
23-
fn from_map(map: BTreeMap<String, String>) -> Result<Output, Error> {
24-
Ok(Output {
25-
id: get_field!(map, "outputid"),
26-
plugin: get_field!(map, "plugin"),
27-
name: map.get("outputname").map(|v| v.to_owned()).ok_or(Error::Proto(ProtoError::NoField("outputname")))?,
28-
enabled: get_field!(map, bool "outputenabled"),
22+
impl FromIter for Output {
23+
// Implement FromIter directly so that we can parse plugin-specific attributes
24+
fn from_iter<I: Iterator<Item = Result<(String, String), Error>>>(iter: I) -> Result<Output, Error> {
25+
let mut attributes = Vec::new();
26+
let mut name: Option<String> = None; // panic if unnamed
27+
let mut plugin: Option<String> = None; // panic if not found
28+
let mut id: u32 = 0;
29+
let mut enabled: bool = false;
30+
31+
for res in iter {
32+
let line = res?;
33+
match &*line.0 {
34+
"outputid" => { id = line.1.parse::<u32>()? },
35+
"outputname" => { name.replace(line.1); },
36+
"plugin" => { plugin.replace(line.1); },
37+
"outputenabled" => enabled = line.1 == "1",
38+
"attribute" => {
39+
let terms: Vec<&str> = line.1.split("=").collect();
40+
if terms.len() != 2 {
41+
return Err(Error::Proto(ProtoError::NotPair));
42+
}
43+
attributes.push((terms[0].to_owned(), terms[1].to_owned()));
44+
},
45+
_ => {}
46+
}
47+
}
48+
49+
if name.is_none() {
50+
return Err(Error::Proto(ProtoError::NoField("outputname")));
51+
}
52+
53+
if plugin.is_none() {
54+
return Err(Error::Proto(ProtoError::NoField("plugin")));
55+
}
56+
57+
Ok(Self {
58+
id,
59+
plugin: plugin.unwrap(),
60+
name: name.unwrap(),
61+
enabled,
62+
attributes
2963
})
3064
}
3165
}

src/search.rs

Lines changed: 89 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#![allow(missing_docs)]
22
// TODO: unfinished functionality
33

4-
use crate::proto::ToArguments;
5-
use std::borrow::Cow;
4+
use crate::proto::{Quoted, ToArguments};
5+
use std::{
6+
io::Write, // implements write for Vec
7+
borrow::Cow
8+
};
69
use std::convert::Into;
710
use std::fmt;
811
use std::result::Result as StdResult;
@@ -17,16 +20,39 @@ pub enum Term<'a> {
1720
Tag(Cow<'a, str>),
1821
}
1922

23+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged, rename_all = "lowercase"))]
24+
pub enum Operation {
25+
Equals,
26+
NotEquals,
27+
Contains,
28+
#[cfg_attr(feature = "serde", serde(rename = "starts_with"))]
29+
StartsWith
30+
}
31+
2032
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
2133
pub struct Filter<'a> {
2234
typ: Term<'a>,
2335
what: Cow<'a, str>,
36+
how: Operation
2437
}
2538

2639
impl<'a> Filter<'a> {
27-
fn new<W>(typ: Term<'a>, what: W) -> Filter
40+
pub fn new<W>(typ: Term<'a>, what: W) -> Filter
2841
where W: 'a + Into<Cow<'a, str>> {
29-
Filter { typ, what: what.into() }
42+
Filter {
43+
typ,
44+
what: what.into(),
45+
how: Operation::Equals
46+
}
47+
}
48+
49+
pub fn new_with_op<W>(typ: Term<'a>, what: W, how: Operation) -> Filter
50+
where W: 'a + Into<Cow<'a, str>> {
51+
Filter {
52+
typ,
53+
what: what.into(),
54+
how
55+
}
3056
}
3157
}
3258

@@ -59,6 +85,11 @@ impl<'a> Query<'a> {
5985
self.filters.push(Filter::new(term, value));
6086
self
6187
}
88+
89+
pub fn and_with_op<'b: 'a, V: 'b + Into<Cow<'b, str>>>(&mut self, term: Term<'b>, op: Operation, value: V) -> &mut Query<'a> {
90+
self.filters.push(Filter::new_with_op(term, value, op));
91+
self
92+
}
6293
}
6394

6495
impl<'a> fmt::Display for Term<'a> {
@@ -80,21 +111,70 @@ impl<'a> ToArguments for &'a Term<'a> {
80111
}
81112
}
82113

114+
impl fmt::Display for Operation {
115+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
116+
f.write_str(match *self {
117+
Operation::Equals => "==",
118+
Operation::NotEquals => "!=",
119+
Operation::Contains => "contains",
120+
Operation::StartsWith => "starts_with"
121+
})
122+
}
123+
}
124+
125+
impl ToArguments for Operation {
126+
fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E>
127+
where F: FnMut(&str) -> StdResult<(), E> {
128+
f(&self.to_string())
129+
}
130+
}
131+
83132
impl<'a> ToArguments for &'a Filter<'a> {
84133
fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E>
85134
where F: FnMut(&str) -> StdResult<(), E> {
86-
(&self.typ).to_arguments(f)?;
87-
f(&self.what)
135+
match self.typ {
136+
// For some terms, the filter clause cannot have an operation
137+
Term::Base | Term::LastMod => {
138+
f(&format!(
139+
"({} {})",
140+
&self.typ,
141+
&Quoted(&self.what).to_string()
142+
))
143+
}
144+
_ => {
145+
f(&format!(
146+
"({} {} {})",
147+
&self.typ,
148+
&self.how,
149+
&Quoted(&self.what).to_string())
150+
)
151+
}
152+
}
88153
}
89154
}
90155

91156
impl<'a> ToArguments for &'a Query<'a> {
157+
// Use MPD 0.21+ filter syntax
92158
fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E>
93159
where F: FnMut(&str) -> StdResult<(), E> {
94-
for filter in &self.filters {
95-
filter.to_arguments(f)?
160+
// Construct the query string in its entirety first before escaping
161+
if !self.filters.is_empty() {
162+
let mut qs = String::new();
163+
for (i, filter) in self.filters.iter().enumerate() {
164+
if i > 0 {
165+
qs.push_str(" AND ");
166+
}
167+
// Leave escaping to the filter since terms should not be escaped or quoted
168+
filter.to_arguments(&mut |arg| {
169+
qs.push_str(arg);
170+
Ok(())
171+
})?;
172+
}
173+
// println!("Singly escaped query string: {}", &qs);
174+
f(&qs)
175+
} else {
176+
Ok(())
96177
}
97-
Ok(())
98178
}
99179
}
100180

src/status.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ impl FromIter for Status {
102102
"duration" => result.duration = line.1.parse::<f32>().ok().map(|v| Duration::from_millis((v * 1000.0) as u64)),
103103
"bitrate" => result.bitrate = Some(line.1.parse()?),
104104
"xfade" => result.crossfade = Some(Duration::from_secs(line.1.parse()?)),
105-
// "mixrampdb" => 0.0, //get_field!(map, "mixrampdb"),
106-
// "mixrampdelay" => None, //get_field!(map, opt "mixrampdelay").map(|v: f64| Duration::milliseconds((v * 1000.0) as i64)),
105+
"mixrampdb" => result.mixrampdb = line.1.parse::<f32>()?,
106+
"mixrampdelay" => result.mixrampdelay = Some(Duration::from_secs_f64(line.1.parse()?)),
107107
"audio" => result.audio = Some(line.1.parse()?),
108108
"updating_db" => result.updating_db = Some(line.1.parse()?),
109109
"error" => result.error = Some(line.1.to_owned()),
@@ -120,17 +120,33 @@ impl FromIter for Status {
120120
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121121
#[derive(Debug, Copy, Clone, PartialEq)]
122122
pub struct AudioFormat {
123-
/// sample rate, kbps
123+
/// Sample rate, kbps.
124+
/// For DSD, to align with MPD's internal handling, the returned rate will be in kilobytes per second instead.
125+
/// See https://mpd.readthedocs.io/en/latest/user.html#audio-output-format.
124126
pub rate: u32,
125-
/// sample resolution in bits, can be 0 for floating point resolution
127+
/// Sample resolution in bits, can be 0 for floating point resolution or 1 for DSD.
126128
pub bits: u8,
127-
/// number of channels
129+
/// Number of channels.
128130
pub chans: u8,
129131
}
130132

131133
impl FromStr for AudioFormat {
132134
type Err = ParseError;
133135
fn from_str(s: &str) -> Result<AudioFormat, ParseError> {
136+
if s.contains("dsd") {
137+
// DSD format string only contains two terms: "dsd..." and number of channels.
138+
// To shoehorn into our current AudioFormat struct, use the following conversion:
139+
// - Sample rate: 44100 * the DSD multiplier / 8. For example, DSD64 is sampled at 2.8224MHz.
140+
// - Bits: 1 (DSD is a sequence of single-bit values, or PDM).
141+
// - Channels: as usual.
142+
let mut it = s.split(':');
143+
let dsd_mul: u32 = it.next().ok_or(ParseError::NoRate).and_then(|v| v[3..].parse().map_err(ParseError::BadRate))?;
144+
return Ok(AudioFormat {
145+
rate: dsd_mul * 44100 / 8,
146+
bits: 1,
147+
chans: it.next().ok_or(ParseError::NoChans).and_then(|v| v.parse().map_err(ParseError::BadChans))?,
148+
});
149+
}
134150
let mut it = s.split(':');
135151
Ok(AudioFormat {
136152
rate: it.next().ok_or(ParseError::NoRate).and_then(|v| v.parse().map_err(ParseError::BadRate))?,

0 commit comments

Comments
 (0)