Skip to content

Commit

Permalink
Merge pull request #74 from htkhiem/main
Browse files Browse the repository at this point in the history
MPD v0.21+ filter syntax, DSD support & more
  • Loading branch information
kstep authored Nov 30, 2024
2 parents 216cdcd + 81d6508 commit 42177f6
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 24 deletions.
54 changes: 44 additions & 10 deletions src/output.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
//! The module describes output
use crate::convert::FromMap;
use crate::convert::FromIter;
use crate::error::{Error, ProtoError};
use std::collections::BTreeMap;
use std::convert::From;

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

impl FromMap for Output {
fn from_map(map: BTreeMap<String, String>) -> Result<Output, Error> {
Ok(Output {
id: get_field!(map, "outputid"),
plugin: get_field!(map, "plugin"),
name: map.get("outputname").map(|v| v.to_owned()).ok_or(Error::Proto(ProtoError::NoField("outputname")))?,
enabled: get_field!(map, bool "outputenabled"),
impl FromIter for Output {
// Implement FromIter directly so that we can parse plugin-specific attributes
fn from_iter<I: Iterator<Item = Result<(String, String), Error>>>(iter: I) -> Result<Output, Error> {
let mut attributes = Vec::new();
let mut name: Option<String> = None; // panic if unnamed
let mut plugin: Option<String> = None; // panic if not found
let mut id: u32 = 0;
let mut enabled: bool = false;

for res in iter {
let line = res?;
match &*line.0 {
"outputid" => { id = line.1.parse::<u32>()? },
"outputname" => { name.replace(line.1); },
"plugin" => { plugin.replace(line.1); },
"outputenabled" => enabled = line.1 == "1",
"attribute" => {
let terms: Vec<&str> = line.1.split("=").collect();
if terms.len() != 2 {
return Err(Error::Proto(ProtoError::NotPair));
}
attributes.push((terms[0].to_owned(), terms[1].to_owned()));
},
_ => {}
}
}

if name.is_none() {
return Err(Error::Proto(ProtoError::NoField("outputname")));
}

if plugin.is_none() {
return Err(Error::Proto(ProtoError::NoField("plugin")));
}

Ok(Self {
id,
plugin: plugin.unwrap(),
name: name.unwrap(),
enabled,
attributes
})
}
}
98 changes: 89 additions & 9 deletions src/search.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#![allow(missing_docs)]
// TODO: unfinished functionality

use crate::proto::ToArguments;
use std::borrow::Cow;
use crate::proto::{Quoted, ToArguments};
use std::{
io::Write, // implements write for Vec
borrow::Cow
};
use std::convert::Into;
use std::fmt;
use std::result::Result as StdResult;
Expand All @@ -17,16 +20,39 @@ pub enum Term<'a> {
Tag(Cow<'a, str>),
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged, rename_all = "lowercase"))]
pub enum Operation {
Equals,
NotEquals,
Contains,
#[cfg_attr(feature = "serde", serde(rename = "starts_with"))]
StartsWith
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Filter<'a> {
typ: Term<'a>,
what: Cow<'a, str>,
how: Operation
}

impl<'a> Filter<'a> {
fn new<W>(typ: Term<'a>, what: W) -> Filter
pub fn new<W>(typ: Term<'a>, what: W) -> Filter
where W: 'a + Into<Cow<'a, str>> {
Filter { typ, what: what.into() }
Filter {
typ,
what: what.into(),
how: Operation::Equals
}
}

pub fn new_with_op<W>(typ: Term<'a>, what: W, how: Operation) -> Filter
where W: 'a + Into<Cow<'a, str>> {
Filter {
typ,
what: what.into(),
how
}
}
}

Expand Down Expand Up @@ -59,6 +85,11 @@ impl<'a> Query<'a> {
self.filters.push(Filter::new(term, value));
self
}

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> {
self.filters.push(Filter::new_with_op(term, value, op));
self
}
}

impl<'a> fmt::Display for Term<'a> {
Expand All @@ -80,21 +111,70 @@ impl<'a> ToArguments for &'a Term<'a> {
}
}

impl fmt::Display for Operation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match *self {
Operation::Equals => "==",
Operation::NotEquals => "!=",
Operation::Contains => "contains",
Operation::StartsWith => "starts_with"
})
}
}

impl ToArguments for Operation {
fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E>
where F: FnMut(&str) -> StdResult<(), E> {
f(&self.to_string())
}
}

impl<'a> ToArguments for &'a Filter<'a> {
fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E>
where F: FnMut(&str) -> StdResult<(), E> {
(&self.typ).to_arguments(f)?;
f(&self.what)
match self.typ {
// For some terms, the filter clause cannot have an operation
Term::Base | Term::LastMod => {
f(&format!(
"({} {})",
&self.typ,
&Quoted(&self.what).to_string()
))
}
_ => {
f(&format!(
"({} {} {})",
&self.typ,
&self.how,
&Quoted(&self.what).to_string())
)
}
}
}
}

impl<'a> ToArguments for &'a Query<'a> {
// Use MPD 0.21+ filter syntax
fn to_arguments<F, E>(&self, f: &mut F) -> StdResult<(), E>
where F: FnMut(&str) -> StdResult<(), E> {
for filter in &self.filters {
filter.to_arguments(f)?
// Construct the query string in its entirety first before escaping
if !self.filters.is_empty() {
let mut qs = String::new();
for (i, filter) in self.filters.iter().enumerate() {
if i > 0 {
qs.push_str(" AND ");
}
// Leave escaping to the filter since terms should not be escaped or quoted
filter.to_arguments(&mut |arg| {
qs.push_str(arg);
Ok(())
})?;
}
// println!("Singly escaped query string: {}", &qs);
f(&qs)
} else {
Ok(())
}
Ok(())
}
}

Expand Down
26 changes: 21 additions & 5 deletions src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ impl FromIter for Status {
"duration" => result.duration = line.1.parse::<f32>().ok().map(|v| Duration::from_millis((v * 1000.0) as u64)),
"bitrate" => result.bitrate = Some(line.1.parse()?),
"xfade" => result.crossfade = Some(Duration::from_secs(line.1.parse()?)),
// "mixrampdb" => 0.0, //get_field!(map, "mixrampdb"),
// "mixrampdelay" => None, //get_field!(map, opt "mixrampdelay").map(|v: f64| Duration::milliseconds((v * 1000.0) as i64)),
"mixrampdb" => result.mixrampdb = line.1.parse::<f32>()?,
"mixrampdelay" => result.mixrampdelay = Some(Duration::from_secs_f64(line.1.parse()?)),
"audio" => result.audio = Some(line.1.parse()?),
"updating_db" => result.updating_db = Some(line.1.parse()?),
"error" => result.error = Some(line.1.to_owned()),
Expand All @@ -120,17 +120,33 @@ impl FromIter for Status {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct AudioFormat {
/// sample rate, kbps
/// Sample rate, kbps.
/// For DSD, to align with MPD's internal handling, the returned rate will be in kilobytes per second instead.
/// See https://mpd.readthedocs.io/en/latest/user.html#audio-output-format.
pub rate: u32,
/// sample resolution in bits, can be 0 for floating point resolution
/// Sample resolution in bits, can be 0 for floating point resolution or 1 for DSD.
pub bits: u8,
/// number of channels
/// Number of channels.
pub chans: u8,
}

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

0 comments on commit 42177f6

Please sign in to comment.