//! Module containing the basic building blocks, definitions and helpers //! for implementing an MPD command. A 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::*; 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; /// 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; /// 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; /// 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 { let (line, rest) = raw .split_once('\n') .ok_or(RequestParserError::UnexpectedEOF)?; debug_assert!(rest.is_empty()); 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; // /// Serializes the response into a Vec. // fn serialize(&self) -> Vec; /// Parses the response from its tokenized parts. /// See also [`parse_raw`]. fn parse(parts: ResponseAttributes<'_>) -> Result; /// Parses the response from its raw byte representation. fn parse_raw(raw: &[u8]) -> Result { 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 { 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::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::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::parse(parts) } /// Parse the raw response string into a response. fn parse_raw_response(raw: &[u8]) -> Result { Self::Response::parse_raw(raw) } async fn execute( request: Self::Request, connection: &mut T, ) -> Result 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; fn into_request_enum(self) -> crate::Request { match Self::COMMAND { $command_name => crate::Request::$name, _ => unimplemented!(), } } fn from_request_enum(request: crate::Request) -> Option { 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( mut parts: crate::commands::RequestTokenizer<'_>, ) -> Result { debug_assert!(parts.next().is_none()); 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 { todo!() } fn parse( _parts: crate::commands::ResponseAttributes<'_>, ) -> Result { 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; 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 { 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 { let item_token = parts .next() .ok_or(crate::commands::RequestParserError::UnexpectedEOF)?; let item = item_token.parse::<$item_type>().map_err(|_| { crate::commands::RequestParserError::SyntaxError(0, item_token.to_owned()) })?; debug_assert!(parts.next().is_none()); 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; 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 { 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 { let item = parts .next() .map(|s| { s.parse().map_err(|_| { crate::commands::RequestParserError::SyntaxError(0, s.to_owned()) }) }) .transpose()?; debug_assert!(parts.next().is_none()); 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 { todo!() } fn parse( parts: crate::commands::ResponseAttributes<'_>, ) -> Result { 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 { todo!() } fn parse( parts: crate::commands::ResponseAttributes<'_>, ) -> Result { // 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(Debug, Clone, PartialEq)] pub enum RequestParserError { SyntaxError(u64, String), MissingCommandListEnd(u64), NestedCommandList(u64), UnexpectedCommandListEnd(u64), UnexpectedEOF, MissingNewline, } // TODO: should these be renamed to fit the mpd docs? // "Attribute" instead of "Property"? #[derive(Error, Debug, Clone, PartialEq)] 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, // 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, ];