mod count;
mod list;
mod playlist;
mod song;
mod sticker;
mod timestamp;
use std::{error::Error, fmt, num::ParseIntError, str::FromStr, sync::Arc, time::Duration};
use bytes::BytesMut;
use mpd_protocol::response::Frame;
pub use self::{
count::Count,
list::{GroupedListValuesIter, List, ListValuesIntoIter, ListValuesIter},
playlist::Playlist,
song::{Song, SongInQueue, SongRange},
sticker::{StickerFind, StickerGet, StickerList},
timestamp::Timestamp,
};
use crate::commands::{ReplayGainMode, SingleMode, SongId, SongPosition};
type KeyValuePair = (Arc<str>, String);
#[derive(Debug)]
pub struct TypedResponseError {
kind: ErrorKind,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl TypedResponseError {
pub fn missing<F>(field: F) -> TypedResponseError
where
F: Into<String>,
{
TypedResponseError {
kind: ErrorKind::Missing {
field: field.into(),
},
source: None,
}
}
pub fn unexpected_field<E, F>(expected: E, found: F) -> TypedResponseError
where
E: Into<String>,
F: Into<String>,
{
TypedResponseError {
kind: ErrorKind::UnexpectedField {
expected: expected.into(),
found: found.into(),
},
source: None,
}
}
pub fn invalid_value<F>(field: F, value: String) -> TypedResponseError
where
F: Into<String>,
{
TypedResponseError {
kind: ErrorKind::InvalidValue {
field: field.into(),
value,
},
source: None,
}
}
pub fn other() -> TypedResponseError {
TypedResponseError {
kind: ErrorKind::Other,
source: None,
}
}
pub fn source<E>(self, source: E) -> TypedResponseError
where
E: Error + Send + Sync + 'static,
{
let source = Some(Box::from(source));
TypedResponseError { source, ..self }
}
}
#[derive(Debug)]
enum ErrorKind {
Missing { field: String },
UnexpectedField { expected: String, found: String },
InvalidValue { field: String, value: String },
Other,
}
impl fmt::Display for TypedResponseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
ErrorKind::Missing { field } => write!(f, "field {field:?} is required but missing"),
ErrorKind::UnexpectedField { expected, found } => {
write!(f, "expected field {expected:?} but found {found:?}")
}
ErrorKind::InvalidValue { field, value } => {
write!(f, "invalid value {value:?} for field {field:?}")
}
ErrorKind::Other => write!(f, "invalid response"),
}
}
}
impl Error for TypedResponseError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source.as_deref().map(|e| e as _)
}
}
pub(crate) trait FromFieldValue: Sized {
fn from_value(v: String, field: &str) -> Result<Self, TypedResponseError>;
}
impl FromFieldValue for bool {
fn from_value(v: String, field: &str) -> Result<Self, TypedResponseError> {
match &*v {
"0" => Ok(false),
"1" => Ok(true),
_ => Err(TypedResponseError::invalid_value(field, v)),
}
}
}
impl FromFieldValue for Duration {
fn from_value(v: String, field: &str) -> Result<Self, TypedResponseError> {
parse_duration(field, v)
}
}
impl FromFieldValue for PlayState {
fn from_value(v: String, field: &str) -> Result<Self, TypedResponseError> {
match &*v {
"play" => Ok(PlayState::Playing),
"pause" => Ok(PlayState::Paused),
"stop" => Ok(PlayState::Stopped),
_ => Err(TypedResponseError::invalid_value(field, v)),
}
}
}
impl FromFieldValue for ReplayGainMode {
fn from_value(v: String, field: &str) -> Result<Self, TypedResponseError> {
match &*v {
"off" => Ok(ReplayGainMode::Off),
"track" => Ok(ReplayGainMode::Track),
"album" => Ok(ReplayGainMode::Album),
"auto" => Ok(ReplayGainMode::Auto),
_ => Err(TypedResponseError::invalid_value(field, v)),
}
}
}
fn parse_integer<I: FromStr<Err = ParseIntError>>(
v: String,
field: &str,
) -> Result<I, TypedResponseError> {
v.parse::<I>()
.map_err(|e| TypedResponseError::invalid_value(field, v).source(e))
}
impl FromFieldValue for u8 {
fn from_value(v: String, field: &str) -> Result<Self, TypedResponseError> {
parse_integer(v, field)
}
}
impl FromFieldValue for u32 {
fn from_value(v: String, field: &str) -> Result<Self, TypedResponseError> {
parse_integer(v, field)
}
}
impl FromFieldValue for u64 {
fn from_value(v: String, field: &str) -> Result<Self, TypedResponseError> {
parse_integer(v, field)
}
}
impl FromFieldValue for usize {
fn from_value(v: String, field: &str) -> Result<Self, TypedResponseError> {
parse_integer(v, field)
}
}
pub(crate) fn value<V: FromFieldValue>(
frame: &mut Frame,
field: &'static str,
) -> Result<V, TypedResponseError> {
let value = frame
.get(field)
.ok_or_else(|| TypedResponseError::missing(field))?;
V::from_value(value, field)
}
fn optional_value<V: FromFieldValue>(
frame: &mut Frame,
field: &'static str,
) -> Result<Option<V>, TypedResponseError> {
match frame.get(field) {
None => Ok(None),
Some(v) => {
let v = V::from_value(v, field)?;
Ok(Some(v))
}
}
}
fn song_identifier(
frame: &mut Frame,
position_field: &'static str,
id_field: &'static str,
) -> Result<Option<(SongPosition, SongId)>, TypedResponseError> {
let position = match optional_value(frame, position_field)? {
Some(p) => SongPosition(p),
None => return Ok(None),
};
let id = value(frame, id_field).map(SongId)?;
Ok(Some((position, id)))
}
fn parse_duration<V: AsRef<str> + Into<String>>(
field: &str,
value: V,
) -> Result<Duration, TypedResponseError> {
let v = match value.as_ref().parse::<f64>() {
Ok(v) => v,
Err(e) => return Err(TypedResponseError::invalid_value(field, value.into()).source(e)),
};
if v >= 0.0 && v <= Duration::MAX.as_secs_f64() && v.is_finite() {
Ok(Duration::from_secs_f64(v))
} else {
Err(TypedResponseError::invalid_value(field, value.into()))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum PlayState {
Stopped,
Playing,
Paused,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
#[non_exhaustive]
pub struct ReplayGainStatus {
pub mode: ReplayGainMode,
}
impl ReplayGainStatus {
pub(crate) fn from_frame(mut raw: Frame) -> Result<Self, TypedResponseError> {
let f = &mut raw;
Ok(Self {
mode: value(f, "replay_gain_mode")?,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
#[non_exhaustive]
pub struct Status {
pub volume: u8,
pub state: PlayState,
pub repeat: bool,
pub random: bool,
pub consume: bool,
pub single: SingleMode,
pub playlist_version: u32,
pub playlist_length: usize,
pub current_song: Option<(SongPosition, SongId)>,
pub next_song: Option<(SongPosition, SongId)>,
pub elapsed: Option<Duration>,
pub duration: Option<Duration>,
pub bitrate: Option<u64>,
pub crossfade: Duration,
pub update_job: Option<u64>,
pub error: Option<String>,
pub partition: Option<String>,
}
impl Status {
pub(crate) fn from_frame(mut raw: Frame) -> Result<Self, TypedResponseError> {
let single = match raw.get("single") {
None => SingleMode::Disabled,
Some(val) => match val.as_str() {
"0" => SingleMode::Disabled,
"1" => SingleMode::Enabled,
"oneshot" => SingleMode::Oneshot,
_ => return Err(TypedResponseError::invalid_value("single", val)),
},
};
let duration = if let Some(val) = raw.get("duration") {
Some(Duration::from_value(val, "duration")?)
} else if let Some(time) = raw.get("Time") {
if let Some((_, duration)) = time.split_once(':') {
Some(Duration::from_value(duration.to_owned(), "Time")?)
} else {
return Err(TypedResponseError::invalid_value("Time", time));
}
} else {
None
};
let f = &mut raw;
Ok(Self {
volume: optional_value(f, "volume")?.unwrap_or(0),
state: value(f, "state")?,
repeat: value(f, "repeat")?,
random: value(f, "random")?,
consume: value(f, "consume")?,
single,
playlist_length: optional_value(f, "playlistlength")?.unwrap_or(0),
playlist_version: optional_value(f, "playlist")?.unwrap_or(0),
current_song: song_identifier(f, "song", "songid")?,
next_song: song_identifier(f, "nextsong", "nextsongid")?,
elapsed: optional_value(f, "elapsed")?,
duration,
bitrate: optional_value(f, "bitrate")?,
crossfade: optional_value(f, "xfade")?.unwrap_or(Duration::ZERO),
update_job: optional_value(f, "update_job")?,
error: f.get("error"),
partition: f.get("partition"),
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
#[non_exhaustive]
pub struct Stats {
pub artists: u64,
pub albums: u64,
pub songs: u64,
pub uptime: Duration,
pub playtime: Duration,
pub db_playtime: Duration,
pub db_last_update: u64,
}
impl Stats {
pub(crate) fn from_frame(mut f: Frame) -> Result<Self, TypedResponseError> {
let f = &mut f;
Ok(Self {
artists: value(f, "artists")?,
albums: value(f, "albums")?,
songs: value(f, "songs")?,
uptime: value(f, "uptime")?,
playtime: value(f, "playtime")?,
db_playtime: value(f, "db_playtime")?,
db_last_update: value(f, "db_update")?,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct AlbumArt {
pub size: usize,
pub mime: Option<String>,
pub data: BytesMut,
}
impl AlbumArt {
pub(crate) fn from_frame(mut frame: Frame) -> Result<Option<Self>, TypedResponseError> {
let Some(data) = frame.take_binary() else {
return Ok(None);
};
Ok(Some(AlbumArt {
size: value(&mut frame, "size")?,
mime: frame.get("type"),
data,
}))
}
}
pub(crate) fn parse_channel_messages<F>(
fields: F,
) -> Result<Vec<(String, String)>, TypedResponseError>
where
F: IntoIterator<Item = KeyValuePair>,
{
let mut response = Vec::new();
let mut fields = fields.into_iter();
while let Some(channel) = fields.next() {
if &*channel.0 != "channel" {
return Err(TypedResponseError::unexpected_field("channel", &*channel.0));
}
let Some(message) = fields.next() else {
return Err(TypedResponseError::missing("message"));
};
if &*message.0 != "message" {
return Err(TypedResponseError::unexpected_field("message", &*message.0));
}
response.push((channel.1, message.1));
}
Ok(response)
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::*;
#[test]
fn duration_parsing() {
assert_eq!(
parse_duration("duration", "1.500").unwrap(),
Duration::from_secs_f64(1.5)
);
assert_eq!(parse_duration("Time", "3").unwrap(), Duration::from_secs(3));
assert_matches!(parse_duration("duration", "asdf"), Err(_));
assert_matches!(parse_duration("duration", "-1"), Err(_));
assert_matches!(parse_duration("duration", "NaN"), Err(_));
assert_matches!(parse_duration("duration", "-1"), Err(_));
}
#[test]
fn channel_message_parsing() {
assert_eq!(parse_channel_messages(Vec::new()).unwrap(), Vec::new());
let fields = vec![
(Arc::from("channel"), String::from("foo")),
(Arc::from("message"), String::from("message 1")),
(Arc::from("channel"), String::from("bar")),
(Arc::from("message"), String::from("message 2")),
];
assert_eq!(
parse_channel_messages(fields).unwrap(),
vec![
(String::from("foo"), String::from("message 1")),
(String::from("bar"), String::from("message 2")),
]
);
}
}