827 lines
27 KiB
Rust
827 lines
27 KiB
Rust
//! The basic building blocks, definitions and helpers used to define an Mpd command.
|
|
//!
|
|
//! An mpd command consists of a pair of serializers and parsers for both the request
|
|
//! and the corresponding response, as well as the command name used to identify the command.
|
|
//!
|
|
//! Each command is modelled as a struct implementing the [`Command`] trait,
|
|
//! which in turn uses the [`CommandRequest`] and [`CommandResponse`] traits
|
|
//! to define the request and response types respectively.
|
|
|
|
use crate::{request_tokenizer::RequestTokenizer, response_tokenizer::ResponseAttributes};
|
|
|
|
mod audio_output_devices;
|
|
mod client_to_client;
|
|
mod connection_settings;
|
|
mod controlling_playback;
|
|
mod mounts_and_neighbors;
|
|
mod music_database;
|
|
mod partition_commands;
|
|
mod playback_options;
|
|
mod querying_mpd_status;
|
|
mod queue;
|
|
mod reflection;
|
|
mod stickers;
|
|
mod stored_playlists;
|
|
|
|
pub use audio_output_devices::*;
|
|
pub use client_to_client::*;
|
|
pub use connection_settings::*;
|
|
pub use controlling_playback::*;
|
|
pub use mounts_and_neighbors::*;
|
|
pub use music_database::*;
|
|
pub use partition_commands::*;
|
|
pub use playback_options::*;
|
|
pub use querying_mpd_status::*;
|
|
pub use queue::*;
|
|
pub use reflection::*;
|
|
use serde::{Deserialize, Serialize};
|
|
pub use stickers::*;
|
|
pub use stored_playlists::*;
|
|
|
|
#[cfg(feature = "futures")]
|
|
use futures_util::{
|
|
AsyncBufReadExt,
|
|
io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader},
|
|
};
|
|
|
|
use thiserror::Error;
|
|
#[cfg(feature = "tokio")]
|
|
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
|
|
|
|
/// A trait modelling a single MPD command request.
|
|
pub(crate) trait CommandRequest
|
|
where
|
|
Self: Sized,
|
|
{
|
|
/// The command name used within the protocol
|
|
const COMMAND: &'static str;
|
|
|
|
// TODO: add these for ease of throwing parsing errors
|
|
/// The minimum number of arguments this command takes
|
|
const MIN_ARGS: u32;
|
|
|
|
/// The maximum number of arguments this command takes.
|
|
///
|
|
/// Note that in the case of keyworded arguments, such as
|
|
/// `group <groupname>`, `sort <sorting>`, etc., these are
|
|
/// counted as a single argument despite being two tokens.
|
|
const MAX_ARGS: Option<u32>;
|
|
|
|
/// Helper function to create a [`RequestParserError::TooManyArguments`] error
|
|
fn too_many_arguments_error(found: u32) -> RequestParserError {
|
|
RequestParserError::TooManyArguments {
|
|
expected_min: Self::MIN_ARGS,
|
|
expected_max: Self::MAX_ARGS,
|
|
found,
|
|
}
|
|
}
|
|
|
|
/// Helper function to throw a [`RequestParserError::TooManyArguments`] error
|
|
fn throw_if_too_many_arguments(parts: RequestTokenizer<'_>) -> Result<(), RequestParserError> {
|
|
let remaining_args = parts.count().try_into().unwrap_or(u32::MAX);
|
|
if remaining_args != 0 {
|
|
return Err(Self::too_many_arguments_error(
|
|
remaining_args.saturating_add(Self::MAX_ARGS.unwrap()),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function to create a [`RequestParserError::MissingArguments`] error
|
|
fn missing_arguments_error(found: u32) -> RequestParserError {
|
|
RequestParserError::MissingArguments {
|
|
expected_min: Self::MIN_ARGS,
|
|
expected_max: Self::MAX_ARGS,
|
|
found,
|
|
}
|
|
}
|
|
|
|
/// Converts this specific request type to it's corresponding variant in the generic Request enum.
|
|
fn into_request_enum(self) -> crate::Request;
|
|
|
|
/// Converts from the generic Request enum to this specific request type.
|
|
///
|
|
/// If the enum variant does not match this type, returns None.
|
|
fn from_request_enum(request: crate::Request) -> Option<Self>;
|
|
|
|
/// Serializes the request into a String.
|
|
fn serialize(&self) -> String;
|
|
|
|
/// Parses the request from its tokenized parts.
|
|
/// See also [`parse_raw`].
|
|
fn parse(parts: RequestTokenizer<'_>) -> Result<Self, RequestParserError>;
|
|
|
|
/// Parses the request from its raw string representation.
|
|
///
|
|
/// This assumes the raw string starts with the command name, e.g.
|
|
/// `command_name arg1 "arg2 arg3"`
|
|
fn parse_raw(raw: &str) -> Result<Self, RequestParserError> {
|
|
let (line, rest) = raw
|
|
.split_once('\n')
|
|
.ok_or(RequestParserError::MissingNewline)?;
|
|
|
|
if line.is_empty() {
|
|
return Err(RequestParserError::EmptyLine);
|
|
}
|
|
|
|
let mut tokenized = RequestTokenizer::new(line);
|
|
|
|
let command_name_token_length = Self::COMMAND.split_ascii_whitespace().count();
|
|
let mut command_name = Vec::with_capacity(command_name_token_length);
|
|
for _ in 0..command_name_token_length {
|
|
let token = tokenized
|
|
.next()
|
|
.ok_or(RequestParserError::SyntaxError(0, line.to_string()))?;
|
|
command_name.push(token);
|
|
}
|
|
let command_name = command_name.join(" ");
|
|
|
|
if command_name != Self::COMMAND {
|
|
return Err(RequestParserError::SyntaxError(0, line.to_string()));
|
|
}
|
|
|
|
Self::parse(tokenized)
|
|
}
|
|
}
|
|
|
|
/// A trait modelling a single MPD command response.
|
|
pub(crate) trait CommandResponse
|
|
where
|
|
Self: Sized,
|
|
{
|
|
/// Converts this specific response type to it's corresponding variant in the generic Response enum.
|
|
fn into_response_enum(self) -> crate::Response;
|
|
|
|
/// Converts from the generic Response enum to this specific response type.
|
|
///
|
|
/// If the enum variant does not match this type, returns None.
|
|
fn from_response_enum(response: crate::Response) -> Option<Self>;
|
|
|
|
// /// Serializes the response into a Vec<u8>.
|
|
// fn serialize(&self) -> Vec<u8>;
|
|
|
|
/// Parses the response from its tokenized parts.
|
|
/// See also [`parse_raw`].
|
|
fn parse(parts: ResponseAttributes<'_>) -> Result<Self, ResponseParserError>;
|
|
|
|
/// Parses the response from its raw byte representation.
|
|
fn parse_raw(raw: &[u8]) -> Result<Self, ResponseParserError> {
|
|
Self::parse(ResponseAttributes::new_from_bytes(raw))
|
|
}
|
|
}
|
|
|
|
/// A trait modelling the request/response pair of a single MPD command.
|
|
pub trait Command {
|
|
/// The request sent from the client to the server
|
|
type Request: CommandRequest;
|
|
/// The response sent from the server to the client
|
|
type Response: CommandResponse;
|
|
|
|
/// The command name used within the protocol
|
|
const COMMAND: &'static str = Self::Request::COMMAND;
|
|
|
|
/// Serialize the request into a string.
|
|
/// This should optimally produce an input that can be parsed by [`parse_request`]
|
|
fn serialize_request(&self, request: Self::Request) -> String {
|
|
request.serialize().to_owned()
|
|
}
|
|
/// Serialize the request into a bytestring.
|
|
fn serialize_request_to_bytes(&self, request: Self::Request) -> Vec<u8> {
|
|
self.serialize_request(request).into_bytes()
|
|
}
|
|
|
|
/// Parse the request from its tokenized parts. See also [`parse_raw_request`].
|
|
fn parse_request(parts: RequestTokenizer) -> Result<Self::Request, RequestParserError> {
|
|
Self::Request::parse(parts)
|
|
}
|
|
|
|
/// Parse the raw request string into a request.
|
|
/// This assumes the raw string starts with the command name, e.g. `command_name arg1 "arg2 arg3"`
|
|
fn parse_raw_request(raw: &str) -> Result<Self::Request, RequestParserError> {
|
|
Self::Request::parse_raw(raw)
|
|
}
|
|
|
|
// /// Serialize the response into a string.
|
|
// fn serialize_response(&self, response: Self::Response) -> String {
|
|
|
|
/// Parse the response from its tokenized parts. See also [`parse_raw_response`].
|
|
fn parse_response(parts: ResponseAttributes) -> Result<Self::Response, ResponseParserError> {
|
|
Self::Response::parse(parts)
|
|
}
|
|
/// Parse the raw response string into a response.
|
|
fn parse_raw_response(raw: &[u8]) -> Result<Self::Response, ResponseParserError> {
|
|
Self::Response::parse_raw(raw)
|
|
}
|
|
|
|
async fn execute<T>(
|
|
request: Self::Request,
|
|
connection: &mut T,
|
|
) -> Result<Self::Response, crate::MpdClientError>
|
|
where
|
|
Self: Sized,
|
|
T: AsyncWrite + AsyncRead + Unpin,
|
|
{
|
|
let payload = request.serialize();
|
|
|
|
connection
|
|
.write_all(payload.as_bytes())
|
|
.await
|
|
.map_err(crate::MpdClientError::ConnectionError)?;
|
|
|
|
connection
|
|
.flush()
|
|
.await
|
|
.map_err(crate::MpdClientError::ConnectionError)?;
|
|
|
|
let mut response_bytes = Vec::new();
|
|
let mut reader = BufReader::new(connection);
|
|
|
|
loop {
|
|
let mut line = Vec::new();
|
|
|
|
let bytes_read = reader
|
|
.read_until(b'\n', &mut line)
|
|
.await
|
|
.map_err(crate::MpdClientError::ConnectionError)?;
|
|
|
|
if bytes_read == 0 {
|
|
break; // EOF reached
|
|
}
|
|
|
|
response_bytes.extend_from_slice(&line);
|
|
|
|
// TODO: handle errors properly
|
|
if line == b"OK\n" || line.starts_with(b"ACK ") {
|
|
break; // End of response
|
|
}
|
|
}
|
|
|
|
let response = Self::parse_raw_response(&response_bytes)
|
|
.map_err(crate::MpdClientError::ResponseParseError)?;
|
|
|
|
Ok(response)
|
|
}
|
|
}
|
|
|
|
// Request/response implementation helpers
|
|
|
|
macro_rules! empty_command_request {
|
|
($name:ident, $command_name:expr) => {
|
|
paste::paste! {
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub struct [<$name Request>];
|
|
}
|
|
|
|
impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } {
|
|
const COMMAND: &'static str = $command_name;
|
|
const MIN_ARGS: u32 = 0;
|
|
const MAX_ARGS: Option<u32> = Some(0);
|
|
|
|
fn into_request_enum(self) -> crate::Request {
|
|
match Self::COMMAND {
|
|
$command_name => crate::Request::$name,
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
|
|
fn from_request_enum(request: crate::Request) -> Option<Self> {
|
|
match (Self::COMMAND, request) {
|
|
($command_name, crate::Request::$name) => {
|
|
Some(paste::paste! { [<$name Request>] })
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn serialize(&self) -> String {
|
|
Self::COMMAND.to_string() + "\n"
|
|
}
|
|
|
|
fn parse(
|
|
parts: crate::commands::RequestTokenizer<'_>,
|
|
) -> Result<Self, crate::commands::RequestParserError> {
|
|
Self::throw_if_too_many_arguments(parts)?;
|
|
|
|
Ok(paste::paste! { [<$name Request>] })
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! empty_command_response {
|
|
($name:ident) => {
|
|
paste::paste! {
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub struct [<$name Response>];
|
|
}
|
|
|
|
impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } {
|
|
fn into_response_enum(self) -> crate::Response {
|
|
todo!()
|
|
}
|
|
|
|
fn from_response_enum(_response: crate::Response) -> Option<Self> {
|
|
todo!()
|
|
}
|
|
|
|
fn parse(
|
|
_parts: crate::commands::ResponseAttributes<'_>,
|
|
) -> Result<Self, crate::commands::ResponseParserError> {
|
|
debug_assert!(_parts.is_empty());
|
|
Ok(paste::paste! { [<$name Response>] })
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! single_item_command_request {
|
|
($name:ident, $command_name:expr, $item_type:ty) => {
|
|
paste::paste! {
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub struct [<$name Request>] ($item_type);
|
|
}
|
|
|
|
impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } {
|
|
const COMMAND: &'static str = $command_name;
|
|
const MIN_ARGS: u32 = 1;
|
|
const MAX_ARGS: Option<u32> = Some(1);
|
|
|
|
fn into_request_enum(self) -> crate::Request {
|
|
match Self::COMMAND {
|
|
$command_name => crate::Request::$name(self.0),
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
|
|
fn from_request_enum(request: crate::Request) -> Option<Self> {
|
|
match (Self::COMMAND, request) {
|
|
($command_name, crate::Request::$name(item)) => {
|
|
Some(paste::paste! { [<$name Request>] ( item ) })
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn serialize(&self) -> String {
|
|
format!("{} {}\n", Self::COMMAND, self.0)
|
|
}
|
|
|
|
fn parse(
|
|
mut parts: crate::commands::RequestTokenizer<'_>,
|
|
) -> Result<Self, crate::commands::RequestParserError> {
|
|
let item_token = parts.next().ok_or(Self::missing_arguments_error(0))?;
|
|
|
|
let item = item_token.parse::<$item_type>().map_err(|_| {
|
|
crate::commands::RequestParserError::SubtypeParserError {
|
|
argument_index: 1,
|
|
expected_type: stringify!($item_type).to_string(),
|
|
raw_input: item_token.to_owned(),
|
|
}
|
|
})?;
|
|
|
|
Self::throw_if_too_many_arguments(parts)?;
|
|
|
|
Ok(paste::paste! { [<$name Request>] ( item ) })
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! single_optional_item_command_request {
|
|
($name:ident, $command_name:expr, $item_type:ty) => {
|
|
paste::paste! {
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub struct [<$name Request>] (Option<$item_type>);
|
|
}
|
|
|
|
impl crate::commands::CommandRequest for paste::paste! { [<$name Request>] } {
|
|
const COMMAND: &'static str = $command_name;
|
|
const MIN_ARGS: u32 = 0;
|
|
const MAX_ARGS: Option<u32> = Some(1);
|
|
|
|
fn into_request_enum(self) -> crate::Request {
|
|
match Self::COMMAND {
|
|
$command_name => crate::Request::$name(self.0),
|
|
_ => unimplemented!(),
|
|
}
|
|
}
|
|
|
|
fn from_request_enum(request: crate::Request) -> Option<Self> {
|
|
match (Self::COMMAND, request) {
|
|
($command_name, crate::Request::$name(item)) => {
|
|
Some(paste::paste! { [<$name Request>] ( item ) })
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn serialize(&self) -> String {
|
|
match &self.0 {
|
|
Some(item) => format!("{} {}\n", Self::COMMAND, item),
|
|
None => Self::COMMAND.to_string() + "\n",
|
|
}
|
|
}
|
|
|
|
fn parse(
|
|
mut parts: crate::commands::RequestTokenizer<'_>,
|
|
) -> Result<Self, crate::commands::RequestParserError> {
|
|
let item = parts
|
|
.next()
|
|
.map(|s| {
|
|
s.parse().map_err(|_| {
|
|
crate::commands::RequestParserError::SubtypeParserError {
|
|
argument_index: 1,
|
|
expected_type: stringify!($item_type).to_string(),
|
|
raw_input: s.to_owned(),
|
|
}
|
|
})
|
|
})
|
|
.transpose()?;
|
|
|
|
Self::throw_if_too_many_arguments(parts)?;
|
|
|
|
Ok(paste::paste! { [<$name Request>] ( item ) })
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! single_item_command_response {
|
|
($name:ident, $item_name:expr, $item_type:ty) => {
|
|
paste::paste! {
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub struct [<$name Response>] ( $item_type );
|
|
}
|
|
|
|
impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } {
|
|
fn into_response_enum(self) -> crate::Response {
|
|
todo!()
|
|
}
|
|
|
|
fn from_response_enum(_response: crate::Response) -> Option<Self> {
|
|
todo!()
|
|
}
|
|
|
|
fn parse(
|
|
parts: crate::commands::ResponseAttributes<'_>,
|
|
) -> Result<Self, crate::commands::ResponseParserError> {
|
|
let map = parts.into_map()?;
|
|
|
|
debug_assert!(map.len() == 1, "Expected only one property in response");
|
|
|
|
let item_token = map.get($item_name).ok_or(
|
|
crate::commands::ResponseParserError::MissingProperty($item_name.to_string()),
|
|
)?;
|
|
let item_ = crate::response_tokenizer::expect_property_type!(
|
|
Some(item_token),
|
|
$item_name,
|
|
Text
|
|
);
|
|
let item = item_.parse::<$item_type>().map_err(|_| {
|
|
crate::commands::ResponseParserError::InvalidProperty(
|
|
$item_name.to_string(),
|
|
item_.to_string(),
|
|
)
|
|
})?;
|
|
|
|
Ok(paste::paste! { [<$name Response>] ( item ) })
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! multi_item_command_response {
|
|
($name:ident, $item_name:expr, $item_type:ty) => {
|
|
paste::paste! {
|
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub struct [<$name Response>] ( Vec<$item_type> );
|
|
}
|
|
|
|
impl crate::commands::CommandResponse for paste::paste! { [<$name Response>] } {
|
|
fn into_response_enum(self) -> crate::Response {
|
|
todo!()
|
|
}
|
|
|
|
fn from_response_enum(_response: crate::Response) -> Option<Self> {
|
|
todo!()
|
|
}
|
|
|
|
fn parse(
|
|
parts: crate::commands::ResponseAttributes<'_>,
|
|
) -> Result<Self, crate::commands::ResponseParserError> {
|
|
// TODO: use lazy vec
|
|
let parts_: Vec<_> = parts.into_vec()?;
|
|
|
|
if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != $item_name) {
|
|
return Err(ResponseParserError::UnexpectedProperty(k.to_string()));
|
|
}
|
|
|
|
let mut items = Vec::with_capacity(parts_.len());
|
|
|
|
let mut iter = parts_.into_iter();
|
|
while let Some(value) = iter.next() {
|
|
let unwrapped_value = expect_property_type!(Some(value.1), $item_name, Text);
|
|
let parsed_value = unwrapped_value.parse::<$item_type>().map_err(|_| {
|
|
crate::commands::ResponseParserError::InvalidProperty(
|
|
$item_name.to_string(),
|
|
unwrapped_value.to_string(),
|
|
)
|
|
})?;
|
|
|
|
items.push(parsed_value);
|
|
}
|
|
|
|
Ok(paste::paste! { [<$name Response>] ( items ) })
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
pub(crate) use empty_command_request;
|
|
pub(crate) use empty_command_response;
|
|
pub(crate) use multi_item_command_response;
|
|
pub(crate) use single_item_command_request;
|
|
pub(crate) use single_item_command_response;
|
|
pub(crate) use single_optional_item_command_request;
|
|
|
|
#[derive(Error, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum RequestParserError {
|
|
#[error("Found empty line while parsing request")]
|
|
EmptyLine,
|
|
|
|
// TODO: remove this, replaced by various other errors
|
|
#[error("Could not parse the request due to a syntax error at position {0}: {1}")]
|
|
SyntaxError(u64, String),
|
|
|
|
// TODO: can we store the parser error as well?
|
|
#[error(
|
|
"Could not parse argument {argument_index} of the request (expected type: {expected_type}, raw input: '{raw_input}')"
|
|
)]
|
|
SubtypeParserError {
|
|
/// The index of the argument that failed to parse
|
|
///
|
|
/// Note that in the case of keyworded arguments, such as
|
|
/// `group <groupname>`, `sort <sorting>`, etc., these are
|
|
/// counted as a single argument despite being two tokens.
|
|
argument_index: u32,
|
|
|
|
/// The expected type of the argument
|
|
expected_type: String,
|
|
|
|
/// The raw input that failed to parse
|
|
raw_input: String,
|
|
},
|
|
|
|
#[error(
|
|
"Too many arguments were provided in the request (expected between {expected_min} and {expected_max:?}, found {found})"
|
|
)]
|
|
TooManyArguments {
|
|
/// The minimum number of arguments that were expected
|
|
expected_min: u32,
|
|
|
|
/// The maximum number of arguments that were expected
|
|
///
|
|
/// This is `None` if the amount of arguments is unbounded.
|
|
expected_max: Option<u32>,
|
|
|
|
/// The number of arguments that were found
|
|
///
|
|
/// Note that in the case of keyworded arguments, such as
|
|
/// `group <groupname>`, `sort <sorting>`, etc., these are
|
|
/// counted as a single argument despite being two tokens.
|
|
found: u32,
|
|
},
|
|
|
|
#[error(
|
|
"Not enough arguments were provided in the request (expected between {expected_min} and {expected_max:?}, found {found})"
|
|
)]
|
|
MissingArguments {
|
|
/// The minimum number of arguments that were expected
|
|
expected_min: u32,
|
|
|
|
/// The maximum number of arguments that were expected.
|
|
///
|
|
/// This is `None` if the amount of arguments is unbounded.
|
|
expected_max: Option<u32>,
|
|
|
|
/// The number of arguments that were found
|
|
///
|
|
/// Note that in the case of keyworded arguments, such as
|
|
/// `group <groupname>`, `sort <sorting>`, etc., these are
|
|
/// counted as a single argument despite being two tokens.
|
|
found: u32,
|
|
},
|
|
|
|
#[error("Keyword argument {keyword} at position {argument_index} is missing its value")]
|
|
MissingKeywordValue {
|
|
/// The unexpected keyword that was found
|
|
keyword: &'static str,
|
|
|
|
/// The index of the argument that was missing it's value
|
|
///
|
|
/// Note that in the case of keyworded arguments, such as
|
|
/// `group <groupname>`, `sort <sorting>`, etc., these are
|
|
/// counted as a single argument despite being two tokens.
|
|
argument_index: u32,
|
|
},
|
|
|
|
#[error("A command list was expected to be closed, but the end was not found")]
|
|
MissingCommandListEnd,
|
|
|
|
#[error("A command list was found inside another command list at line {line}")]
|
|
NestedCommandList {
|
|
/// The line where the nested command list was found
|
|
line: u32,
|
|
},
|
|
|
|
#[error("An unexpected command list end was found")]
|
|
UnexpectedCommandListEnd,
|
|
|
|
// TODO: remove this, replaced by EmptyLine + MissingArguments
|
|
#[error("Request ended early, while more arguments were expected")]
|
|
UnexpectedEOF,
|
|
|
|
#[error("Request is missing terminating newline")]
|
|
MissingNewline,
|
|
}
|
|
|
|
// TODO: should these be renamed to fit the mpd docs?
|
|
// "Attribute" instead of "Property"?
|
|
#[derive(Error, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum ResponseParserError {
|
|
#[error("A property was expected to be present in the response, but was not found: {0}")]
|
|
MissingProperty(String),
|
|
|
|
// TODO: change name to UnexpectedPropertyEncoding
|
|
#[error(
|
|
"An expected property was found in the response, but its encoding was not as expected: {0}: {1}"
|
|
)]
|
|
UnexpectedPropertyType(String, String),
|
|
|
|
#[error("A property was found in the response that was not expected: {0}")]
|
|
UnexpectedProperty(String),
|
|
|
|
#[error("A property was found multiple times in the response, but was only expected once: {0}")]
|
|
DuplicateProperty(String),
|
|
|
|
#[error("The property value is parsable, but the value is invalid or nonsensical: {0}: {1}")]
|
|
InvalidProperty(String, String),
|
|
|
|
#[error("Could not parse the response due to a syntax error at position {0}: {1}")]
|
|
SyntaxError(u64, String),
|
|
|
|
#[error("Response ended early, while more properties were expected")]
|
|
UnexpectedEOF,
|
|
// #[error("Response is missing terminating newline")]
|
|
// MissingNewline,
|
|
}
|
|
|
|
/*******************/
|
|
|
|
pub const COMMAND_NAMES: &[&str] = &[
|
|
// Audio output devices
|
|
DisableOutput::COMMAND,
|
|
EnableOutput::COMMAND,
|
|
Outputs::COMMAND,
|
|
OutputSet::COMMAND,
|
|
ToggleOutput::COMMAND,
|
|
// Client to client
|
|
Channels::COMMAND,
|
|
ReadMessages::COMMAND,
|
|
SendMessage::COMMAND,
|
|
Subscribe::COMMAND,
|
|
Unsubscribe::COMMAND,
|
|
// Connection settings
|
|
BinaryLimit::COMMAND,
|
|
Close::COMMAND,
|
|
Kill::COMMAND,
|
|
Password::COMMAND,
|
|
Ping::COMMAND,
|
|
Protocol::COMMAND,
|
|
ProtocolAll::COMMAND,
|
|
ProtocolAvailable::COMMAND,
|
|
ProtocolClear::COMMAND,
|
|
ProtocolDisable::COMMAND,
|
|
ProtocolEnable::COMMAND,
|
|
TagTypes::COMMAND,
|
|
TagTypesAll::COMMAND,
|
|
TagTypesAvailable::COMMAND,
|
|
TagTypesClear::COMMAND,
|
|
TagTypesDisable::COMMAND,
|
|
TagTypesEnable::COMMAND,
|
|
TagTypesReset::COMMAND,
|
|
// Controlling playback
|
|
Next::COMMAND,
|
|
Pause::COMMAND,
|
|
Play::COMMAND,
|
|
PlayId::COMMAND,
|
|
Previous::COMMAND,
|
|
Seek::COMMAND,
|
|
SeekCur::COMMAND,
|
|
SeekId::COMMAND,
|
|
Stop::COMMAND,
|
|
// Mounts and neighbors
|
|
ListMounts::COMMAND,
|
|
ListNeighbors::COMMAND,
|
|
Mount::COMMAND,
|
|
Unmount::COMMAND,
|
|
// Music database
|
|
AlbumArt::COMMAND,
|
|
Count::COMMAND,
|
|
Find::COMMAND,
|
|
FindAdd::COMMAND,
|
|
GetFingerprint::COMMAND,
|
|
List::COMMAND,
|
|
ListAll::COMMAND,
|
|
ListAllInfo::COMMAND,
|
|
ListFiles::COMMAND,
|
|
LsInfo::COMMAND,
|
|
ReadComments::COMMAND,
|
|
ReadPicture::COMMAND,
|
|
Rescan::COMMAND,
|
|
Search::COMMAND,
|
|
SearchAdd::COMMAND,
|
|
SearchAddPl::COMMAND,
|
|
SearchCount::COMMAND,
|
|
Update::COMMAND,
|
|
// Partition commands
|
|
DelPartition::COMMAND,
|
|
ListPartitions::COMMAND,
|
|
MoveOutput::COMMAND,
|
|
NewPartition::COMMAND,
|
|
Partition::COMMAND,
|
|
// Playback options
|
|
Consume::COMMAND,
|
|
Crossfade::COMMAND,
|
|
GetVol::COMMAND,
|
|
MixRampDb::COMMAND,
|
|
MixRampDelay::COMMAND,
|
|
Random::COMMAND,
|
|
Repeat::COMMAND,
|
|
ReplayGainMode::COMMAND,
|
|
ReplayGainStatus::COMMAND,
|
|
SetVol::COMMAND,
|
|
Single::COMMAND,
|
|
Volume::COMMAND,
|
|
// Querying mpd status
|
|
ClearError::COMMAND,
|
|
CurrentSong::COMMAND,
|
|
Idle::COMMAND,
|
|
Stats::COMMAND,
|
|
Status::COMMAND,
|
|
// Queue
|
|
Add::COMMAND,
|
|
AddId::COMMAND,
|
|
AddTagId::COMMAND,
|
|
Clear::COMMAND,
|
|
ClearTagId::COMMAND,
|
|
Delete::COMMAND,
|
|
DeleteId::COMMAND,
|
|
Move::COMMAND,
|
|
MoveId::COMMAND,
|
|
Playlist::COMMAND,
|
|
PlaylistFind::COMMAND,
|
|
PlaylistId::COMMAND,
|
|
PlaylistInfo::COMMAND,
|
|
PlaylistSearch::COMMAND,
|
|
PlChanges::COMMAND,
|
|
PlChangesPosId::COMMAND,
|
|
Prio::COMMAND,
|
|
PrioId::COMMAND,
|
|
RangeId::COMMAND,
|
|
Shuffle::COMMAND,
|
|
Swap::COMMAND,
|
|
SwapId::COMMAND,
|
|
// Reflection
|
|
Commands::COMMAND,
|
|
Config::COMMAND,
|
|
Decoders::COMMAND,
|
|
NotCommands::COMMAND,
|
|
UrlHandlers::COMMAND,
|
|
// Stickers
|
|
StickerDec::COMMAND,
|
|
StickerDelete::COMMAND,
|
|
StickerFind::COMMAND,
|
|
StickerGet::COMMAND,
|
|
StickerInc::COMMAND,
|
|
StickerList::COMMAND,
|
|
StickerSet::COMMAND,
|
|
StickerNames::COMMAND,
|
|
StickerNamesTypes::COMMAND,
|
|
StickerTypes::COMMAND,
|
|
// Stored playlists
|
|
ListPlaylist::COMMAND,
|
|
ListPlaylistInfo::COMMAND,
|
|
ListPlaylists::COMMAND,
|
|
Load::COMMAND,
|
|
PlaylistAdd::COMMAND,
|
|
PlaylistClear::COMMAND,
|
|
PlaylistDelete::COMMAND,
|
|
PlaylistLength::COMMAND,
|
|
PlaylistMove::COMMAND,
|
|
Rename::COMMAND,
|
|
Rm::COMMAND,
|
|
Save::COMMAND,
|
|
SearchPlaylist::COMMAND,
|
|
];
|