diff --git a/Cargo.lock b/Cargo.lock index 5f73f80..10edf78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,13 +2,37 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "empidee" version = "0.1.0" dependencies = [ + "indoc", + "pretty_assertions", "serde", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.88" @@ -63,3 +87,9 @@ name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/Cargo.toml b/Cargo.toml index 7e96ce4..70a3bfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -serde = "1.0.210" +serde = { version = "1.0.210", features = ["derive"] } + +[dev-dependencies] +indoc = "2.0.5" +pretty_assertions = "1.4.1" diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..7d2ce2c --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,196 @@ +use std::{collections::HashMap, str::SplitWhitespace}; + +use crate::Request; + +mod audio_output_devices; +mod client_to_client; +mod controlling_playback; +mod playback_options; +mod querying_mpd_status; + +pub use querying_mpd_status::clearerror::ClearError; +pub use querying_mpd_status::idle::Idle; +pub use querying_mpd_status::status::Status; + +pub trait Command { + type Response; + // The command name used within the protocol + const COMMAND: &'static str; + + // A function to parse the remaining parts of the command, split by whitespace + fn parse_request(parts: SplitWhitespace<'_>) -> RequestParserResult<'_>; + + fn parse_raw_request(raw: &str) -> RequestParserResult<'_> { + let (line, rest) = raw + .split_once('\n') + .ok_or(RequestParserError::UnexpectedEOF)?; + let mut parts = line.split_whitespace(); + + let command_name = parts + .next() + .ok_or(RequestParserError::SyntaxError(0, line.to_string()))? + .trim(); + + debug_assert!(command_name == Self::COMMAND); + + Self::parse_request(parts).map(|(req, _)| (req, rest)) + } + + // TODO: Replace the HashMap datastructure with something that allows keeping + // duplicate keys and order of insertion + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result>; + + fn parse_raw_response(raw: &str) -> Result> { + let mut parts = HashMap::new(); + let mut lines = raw.lines(); + loop { + let line = lines.next().ok_or(ResponseParserError::UnexpectedEOF)?; + if line.is_empty() { + println!("Warning: empty line in response"); + continue; + } + + if line == "OK" { + break; + } + + let mut keyval = line.splitn(2, ": "); + let key = keyval + .next() + .ok_or(ResponseParserError::SyntaxError(0, line))?; + let value = keyval + .next() + .ok_or(ResponseParserError::SyntaxError(0, line))?; + + parts.insert(key, GenericResponseValue::Text(value)); + } + + Self::parse_response(parts) + } +} + +pub type RequestParserResult<'a> = Result<(Request, &'a str), RequestParserError>; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestParserError { + SyntaxError(u64, String), + MissingCommandListEnd(u64), + NestedCommandList(u64), + UnexpectedCommandListEnd(u64), + UnexpectedEOF, + MissingNewline, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseParserError<'a> { + MissingProperty(&'a str), + UnexpectedPropertyType(&'a str, &'a str), + InvalidProperty(&'a str, &'a str), + SyntaxError(u64, &'a str), + UnexpectedEOF, + MissingNewline, +} + +pub type GenericResponseResult<'a> = Result, &'a str>; + +pub type GenericResponse<'a> = HashMap<&'a str, GenericResponseValue<'a>>; + +#[derive(Debug, Clone, PartialEq)] +pub enum GenericResponseValue<'a> { + Text(&'a str), + Binary(&'a [u8]), + Many(Vec>), +} + +macro_rules! get_property { + ($parts:expr, $name:literal, $variant:ident) => { + match $parts.get($name) { + Some(GenericResponseValue::$variant(value)) => *value, + Some(value) => { + let actual_type = match value { + GenericResponseValue::Text(_) => "Text", + GenericResponseValue::Binary(_) => "Binary", + GenericResponseValue::Many(_) => "Many", + }; + return Err(ResponseParserError::UnexpectedPropertyType( + $name, + actual_type, + )); + } + None => return Err(ResponseParserError::MissingProperty($name)), + } + }; +} + +macro_rules! get_optional_property { + ($parts:expr, $name:literal, $variant:ident) => { + match $parts.get($name) { + Some(GenericResponseValue::$variant(value)) => Some(*value), + Some(value) => { + let actual_type = match value { + GenericResponseValue::Text(_) => "Text", + GenericResponseValue::Binary(_) => "Binary", + GenericResponseValue::Many(_) => "Many", + }; + return Err(ResponseParserError::UnexpectedPropertyType( + $name, + actual_type, + )); + } + None => None, + } + }; +} + +macro_rules! get_and_parse_property { + ($parts:ident, $name:literal, $variant:ident) => { + match $parts.get($name) { + Some(GenericResponseValue::$variant(value)) => (*value) + .parse() + .map_err(|_| ResponseParserError::InvalidProperty($name, value))?, + Some(value) => { + let actual_type = match value { + GenericResponseValue::Text(_) => "Text", + GenericResponseValue::Binary(_) => "Binary", + GenericResponseValue::Many(_) => "Many", + }; + return Err(ResponseParserError::UnexpectedPropertyType( + $name, + actual_type, + )); + } + None => return Err(ResponseParserError::MissingProperty($name)), + } + }; +} + +macro_rules! get_and_parse_optional_property { + ($parts:ident, $name:literal, $variant:ident) => { + match $parts.get($name) { + Some(GenericResponseValue::$variant(value)) => Some( + (*value) + .parse() + .map_err(|_| ResponseParserError::InvalidProperty($name, value))?, + ), + Some(value) => { + let actual_type = match value { + GenericResponseValue::Text(_) => "Text", + GenericResponseValue::Binary(_) => "Binary", + GenericResponseValue::Many(_) => "Many", + }; + return Err(ResponseParserError::UnexpectedPropertyType( + $name, + actual_type, + )); + } + None => None, + } + }; +} + +pub(crate) use get_and_parse_optional_property; +pub(crate) use get_and_parse_property; +pub(crate) use get_optional_property; +pub(crate) use get_property; diff --git a/src/commands/audio_output_devices.rs b/src/commands/audio_output_devices.rs new file mode 100644 index 0000000..872fbb2 --- /dev/null +++ b/src/commands/audio_output_devices.rs @@ -0,0 +1,5 @@ +pub mod disableoutput; +pub mod enableoutput; +pub mod outputs; +pub mod outputset; +pub mod toggleoutput; diff --git a/src/commands/audio_output_devices/disableoutput.rs b/src/commands/audio_output_devices/disableoutput.rs new file mode 100644 index 0000000..ceb7362 --- /dev/null +++ b/src/commands/audio_output_devices/disableoutput.rs @@ -0,0 +1,28 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct DisableOutput; + +impl Command for DisableOutput { + type Response = (); + const COMMAND: &'static str = "disableoutput"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::DisableOutput(output_id.to_string()), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/audio_output_devices/enableoutput.rs b/src/commands/audio_output_devices/enableoutput.rs new file mode 100644 index 0000000..b2c9034 --- /dev/null +++ b/src/commands/audio_output_devices/enableoutput.rs @@ -0,0 +1,28 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct EnableOutput; + +impl Command for EnableOutput { + type Response = (); + const COMMAND: &'static str = "enableoutput"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::EnableOutput(output_id.to_string()), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/audio_output_devices/outputs.rs b/src/commands/audio_output_devices/outputs.rs new file mode 100644 index 0000000..3a388ab --- /dev/null +++ b/src/commands/audio_output_devices/outputs.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +pub struct Outputs; + +#[derive(Debug)] +pub struct Output { + pub id: u64, + pub name: String, + pub plugin: String, + pub enabled: bool, + pub attribute: HashMap, +} + +pub type OutputsResponse = Vec; + +impl Command for Outputs { + type Response = OutputsResponse; + const COMMAND: &'static str = "outputs"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + Ok((Request::Outputs, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + todo!() + // debug_assert!(parts.is_empty()); + // Ok(()) + } +} diff --git a/src/commands/audio_output_devices/outputset.rs b/src/commands/audio_output_devices/outputset.rs new file mode 100644 index 0000000..0833936 --- /dev/null +++ b/src/commands/audio_output_devices/outputset.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct OutputSet; + +impl Command for OutputSet { + type Response = (); + const COMMAND: &'static str = "outputset"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let attribute_name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let attribute_value = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + + debug_assert!(parts.next().is_none()); + + Ok(( + Request::OutputSet( + output_id.to_string(), + attribute_name.to_string(), + attribute_value.to_string(), + ), + "", + )) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/audio_output_devices/toggleoutput.rs b/src/commands/audio_output_devices/toggleoutput.rs new file mode 100644 index 0000000..43586af --- /dev/null +++ b/src/commands/audio_output_devices/toggleoutput.rs @@ -0,0 +1,28 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct ToggleOutput; + +impl Command for ToggleOutput { + type Response = (); + const COMMAND: &'static str = "toggleoutput"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let output_id = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::ToggleOutput(output_id.to_string()), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/client_to_client.rs b/src/commands/client_to_client.rs new file mode 100644 index 0000000..daf105c --- /dev/null +++ b/src/commands/client_to_client.rs @@ -0,0 +1,5 @@ +pub mod channels; +pub mod readmessages; +pub mod sendmessage; +pub mod subscribe; +pub mod unsubscribe; diff --git a/src/commands/client_to_client/channels.rs b/src/commands/client_to_client/channels.rs new file mode 100644 index 0000000..220f1e2 --- /dev/null +++ b/src/commands/client_to_client/channels.rs @@ -0,0 +1,40 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +pub struct Channels; + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ChannelsResponse { + pub channels: Vec, +} + +impl Command for Channels { + type Response = ChannelsResponse; + const COMMAND: &'static str = "channels"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + + Ok((Request::Channels, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + todo!() + // let channels = parts + // .get("channels") + // .ok_or(ResponseParserError::MissingField("channels"))? + // .as_list() + // .ok_or(ResponseParserError::UnexpectedType("channels", "list"))? + // .iter() + // .map(|v| v.as_text().map(ToOwned::to_owned)) + // .collect::>>() + // .ok_or(ResponseParserError::UnexpectedType("channels", "text"))?; + + // Ok(ChannelsResponse { channels }) + } +} diff --git a/src/commands/client_to_client/readmessages.rs b/src/commands/client_to_client/readmessages.rs new file mode 100644 index 0000000..183c8d0 --- /dev/null +++ b/src/commands/client_to_client/readmessages.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +pub struct ReadMessages; + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ReadMessagesResponse { + pub messages: Vec<(String, String)>, +} + +impl Command for ReadMessages { + type Response = ReadMessagesResponse; + const COMMAND: &'static str = "readmessages"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + + Ok((Request::ReadMessages, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + todo!() + } +} diff --git a/src/commands/client_to_client/sendmessage.rs b/src/commands/client_to_client/sendmessage.rs new file mode 100644 index 0000000..2f1c5ee --- /dev/null +++ b/src/commands/client_to_client/sendmessage.rs @@ -0,0 +1,31 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct SendMessage; + +impl Command for SendMessage { + type Response = (); + const COMMAND: &'static str = "sendmessage"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let channel = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + + // TODO: SplitWhitespace::remainder() is unstable, use when stable + let message = parts.collect::>().join(" "); + + debug_assert!(!message.is_empty()); + + Ok((Request::SendMessage(channel.to_string(), message), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/client_to_client/subscribe.rs b/src/commands/client_to_client/subscribe.rs new file mode 100644 index 0000000..96d94da --- /dev/null +++ b/src/commands/client_to_client/subscribe.rs @@ -0,0 +1,28 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct Subscribe; + +impl Command for Subscribe { + type Response = (); + const COMMAND: &'static str = "subscribe"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let channel_name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::Subscribe(channel_name.to_string()), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/client_to_client/unsubscribe.rs b/src/commands/client_to_client/unsubscribe.rs new file mode 100644 index 0000000..d688e26 --- /dev/null +++ b/src/commands/client_to_client/unsubscribe.rs @@ -0,0 +1,28 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct Unsubscribe; + +impl Command for Unsubscribe { + type Response = (); + const COMMAND: &'static str = "unsubscribe"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let channel_name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::Unsubscribe(channel_name.to_string()), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/controlling_playback.rs b/src/commands/controlling_playback.rs new file mode 100644 index 0000000..1437847 --- /dev/null +++ b/src/commands/controlling_playback.rs @@ -0,0 +1,9 @@ +pub mod next; +pub mod pause; +pub mod play; +pub mod playid; +pub mod previous; +pub mod seek; +pub mod seekcur; +pub mod seekid; +pub mod stop; diff --git a/src/commands/controlling_playback/next.rs b/src/commands/controlling_playback/next.rs new file mode 100644 index 0000000..667133d --- /dev/null +++ b/src/commands/controlling_playback/next.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +pub struct Next; + +impl Command for Next { + type Response = (); + const COMMAND: &'static str = "next"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + Ok((Request::Next, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/controlling_playback/pause.rs b/src/commands/controlling_playback/pause.rs new file mode 100644 index 0000000..eb2d81c --- /dev/null +++ b/src/commands/controlling_playback/pause.rs @@ -0,0 +1,29 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct Pause; + +impl Command for Pause { + type Response = (); + const COMMAND: &'static str = "pause"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + match parts.next() { + Some("0") => Ok((Request::Pause(Some(false)), "")), + Some("1") => Ok((Request::Pause(Some(true)), "")), + Some(s) => Err(RequestParserError::SyntaxError(0, s.to_string())), + None => Ok((Request::Pause(None), "")), + } + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/controlling_playback/play.rs b/src/commands/controlling_playback/play.rs new file mode 100644 index 0000000..9460e7b --- /dev/null +++ b/src/commands/controlling_playback/play.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use crate::{ + commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, + }, + common::SongPosition, +}; + +pub struct Play; + +impl Command for Play { + type Response = (); + const COMMAND: &'static str = "play"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let songpos = match parts.next() { + Some(s) => s + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::Play(songpos), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/controlling_playback/playid.rs b/src/commands/controlling_playback/playid.rs new file mode 100644 index 0000000..94e9c62 --- /dev/null +++ b/src/commands/controlling_playback/playid.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use crate::{ + commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, + }, + common::SongId, +}; + +pub struct PlayId; + +impl Command for PlayId { + type Response = (); + const COMMAND: &'static str = "playid"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let songid = match parts.next() { + Some(s) => s + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::PlayId(songid), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/controlling_playback/previous.rs b/src/commands/controlling_playback/previous.rs new file mode 100644 index 0000000..ec70a97 --- /dev/null +++ b/src/commands/controlling_playback/previous.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +pub struct Previous; + +impl Command for Previous { + type Response = (); + const COMMAND: &'static str = "previous"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + Ok((Request::Previous, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/controlling_playback/seek.rs b/src/commands/controlling_playback/seek.rs new file mode 100644 index 0000000..8a92ab3 --- /dev/null +++ b/src/commands/controlling_playback/seek.rs @@ -0,0 +1,41 @@ +use std::collections::HashMap; + +use crate::{ + commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, + }, + common::{SongPosition, TimeWithFractions}, +}; + +pub struct Seek; + +impl Command for Seek { + type Response = (); + const COMMAND: &'static str = "seek"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let songpos = match parts.next() { + Some(s) => s + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + let time = match parts.next() { + Some(t) => t + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + Ok((Request::Seek(songpos, time), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/controlling_playback/seekcur.rs b/src/commands/controlling_playback/seekcur.rs new file mode 100644 index 0000000..48c9489 --- /dev/null +++ b/src/commands/controlling_playback/seekcur.rs @@ -0,0 +1,54 @@ +use std::collections::HashMap; + +use crate::{ + commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, + }, + common::TimeWithFractions, + request::SeekMode, +}; + +pub struct SeekCur; + +impl Command for SeekCur { + type Response = (); + const COMMAND: &'static str = "seekcur"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let time_raw = match parts.next() { + Some(t) => t, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + // TODO: DRY + let (mode, time) = match time_raw { + t if t.starts_with('+') => ( + SeekMode::Relative, + t[1..] + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?, + ), + t if t.starts_with('-') => ( + SeekMode::Relative, + -t[1..] + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?, + ), + t => ( + SeekMode::Absolute, + t.parse::() + .map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?, + ), + }; + + Ok((Request::SeekCur(mode, time), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/controlling_playback/seekid.rs b/src/commands/controlling_playback/seekid.rs new file mode 100644 index 0000000..51b7f94 --- /dev/null +++ b/src/commands/controlling_playback/seekid.rs @@ -0,0 +1,41 @@ +use std::collections::HashMap; + +use crate::{ + commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, + }, + common::{SongId, TimeWithFractions}, +}; + +pub struct SeekId; + +impl Command for SeekId { + type Response = (); + const COMMAND: &'static str = "seekid"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let songid = match parts.next() { + Some(s) => s + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + let time = match parts.next() { + Some(t) => t + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + Ok((Request::SeekId(songid, time), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/controlling_playback/stop.rs b/src/commands/controlling_playback/stop.rs new file mode 100644 index 0000000..c4c246a --- /dev/null +++ b/src/commands/controlling_playback/stop.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +pub struct Stop; + +impl Command for Stop { + type Response = (); + const COMMAND: &'static str = "stop"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + Ok((Request::Stop, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/playback_options.rs b/src/commands/playback_options.rs new file mode 100644 index 0000000..408c2e1 --- /dev/null +++ b/src/commands/playback_options.rs @@ -0,0 +1,12 @@ +pub mod consume; +pub mod crossfade; +pub mod getvol; +pub mod mixrampdb; +pub mod mixrampdelay; +pub mod random; +pub mod repeat; +pub mod replay_gain_mode; +pub mod replay_gain_status; +pub mod setvol; +pub mod single; +pub mod volume; diff --git a/src/commands/playback_options/consume.rs b/src/commands/playback_options/consume.rs new file mode 100644 index 0000000..ced63b1 --- /dev/null +++ b/src/commands/playback_options/consume.rs @@ -0,0 +1,32 @@ +use std::{collections::HashMap, str::FromStr}; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct Consume; + +impl Command for Consume { + type Response = (); + const COMMAND: &'static str = "consume"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let state = match parts.next() { + Some(s) => crate::common::BoolOrOneshot::from_str(s) + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::Consume(state), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/playback_options/crossfade.rs b/src/commands/playback_options/crossfade.rs new file mode 100644 index 0000000..6779d2c --- /dev/null +++ b/src/commands/playback_options/crossfade.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use crate::{ + commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, + }, + common::Seconds, +}; + +pub struct Crossfade; + +impl Command for Crossfade { + type Response = (); + const COMMAND: &'static str = "crossfade"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let seconds = match parts.next() { + Some(s) => s + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::Crossfade(seconds), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/playback_options/getvol.rs b/src/commands/playback_options/getvol.rs new file mode 100644 index 0000000..36e319e --- /dev/null +++ b/src/commands/playback_options/getvol.rs @@ -0,0 +1,31 @@ +use std::collections::HashMap; + +use crate::{ + commands::{Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError}, + request::Volume, +}; + +pub struct GetVol; + +impl Command for GetVol { + type Response = Volume; + const COMMAND: &'static str = "getvol"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + Ok((Request::GetVol, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + todo!() + // let volume = get_property!(parts, Volume, "volume"); + // let volume = match parts.get("volume") { + // Some(GenericResponseValue::Volume(v)) => *v, + // _ => return Err(ResponseParserError::MissingField("volume")), + // }; + + // Ok(volume) + } +} diff --git a/src/commands/playback_options/mixrampdb.rs b/src/commands/playback_options/mixrampdb.rs new file mode 100644 index 0000000..165b674 --- /dev/null +++ b/src/commands/playback_options/mixrampdb.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct MixRampDb; + +impl Command for MixRampDb { + type Response = (); + const COMMAND: &'static str = "mixrampdb"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let db = match parts.next() { + Some(s) => s + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::MixRampDb(db), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/playback_options/mixrampdelay.rs b/src/commands/playback_options/mixrampdelay.rs new file mode 100644 index 0000000..4e381d3 --- /dev/null +++ b/src/commands/playback_options/mixrampdelay.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use crate::{ + commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, + }, + common::Seconds, +}; + +pub struct MixRampDelay; + +impl Command for MixRampDelay { + type Response = (); + const COMMAND: &'static str = "mixrampdelay"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let seconds = match parts.next() { + Some(s) => s + .parse::() + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::MixRampDelay(seconds), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/playback_options/random.rs b/src/commands/playback_options/random.rs new file mode 100644 index 0000000..a2521a9 --- /dev/null +++ b/src/commands/playback_options/random.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct Random; + +impl Command for Random { + type Response = (); + const COMMAND: &'static str = "random"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let state = match parts.next() { + Some("0") => false, + Some("1") => true, + Some(s) => return Err(RequestParserError::SyntaxError(0, s.to_owned())), + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::Random(state), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/playback_options/repeat.rs b/src/commands/playback_options/repeat.rs new file mode 100644 index 0000000..65646a7 --- /dev/null +++ b/src/commands/playback_options/repeat.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct Repeat; + +impl Command for Repeat { + type Response = (); + const COMMAND: &'static str = "repeat"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let state = match parts.next() { + Some("0") => false, + Some("1") => true, + Some(s) => return Err(RequestParserError::SyntaxError(0, s.to_owned())), + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::Repeat(state), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/playback_options/replay_gain_mode.rs b/src/commands/playback_options/replay_gain_mode.rs new file mode 100644 index 0000000..db9e81f --- /dev/null +++ b/src/commands/playback_options/replay_gain_mode.rs @@ -0,0 +1,35 @@ +use std::{collections::HashMap, str::FromStr}; + +use crate::{ + commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, + }, + request::ReplayGainModeMode, +}; + +pub struct ReplayGainMode; + +impl Command for ReplayGainMode { + type Response = (); + const COMMAND: &'static str = "replay_gain_mode"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let mode = match parts.next() { + Some(s) => ReplayGainModeMode::from_str(s) + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::ReplayGainMode(mode), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/playback_options/replay_gain_status.rs b/src/commands/playback_options/replay_gain_status.rs new file mode 100644 index 0000000..7d9d806 --- /dev/null +++ b/src/commands/playback_options/replay_gain_status.rs @@ -0,0 +1,37 @@ +use std::{collections::HashMap, str::FromStr}; + +use crate::{ + commands::{ + get_property, Command, GenericResponseValue, Request, RequestParserResult, + ResponseParserError, + }, + request::ReplayGainModeMode, +}; + +pub struct ReplayGainStatus; + +pub struct ReplayGainStatusResponse { + pub replay_gain_mode: ReplayGainModeMode, +} + +impl Command for ReplayGainStatus { + type Response = ReplayGainStatusResponse; + const COMMAND: &'static str = "replay_gain_status"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + Ok((Request::ReplayGainStatus, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + let replay_gain_mode = get_property!(parts, "replay_gain_mode", Text); + + Ok(ReplayGainStatusResponse { + replay_gain_mode: ReplayGainModeMode::from_str(replay_gain_mode).map_err(|_| { + ResponseParserError::InvalidProperty("replay_gain_mode", replay_gain_mode) + })?, + }) + } +} diff --git a/src/commands/playback_options/setvol.rs b/src/commands/playback_options/setvol.rs new file mode 100644 index 0000000..1c1fdb0 --- /dev/null +++ b/src/commands/playback_options/setvol.rs @@ -0,0 +1,36 @@ +use std::{collections::HashMap, str::FromStr}; + +use crate::{ + commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, + }, + request::Volume, +}; + +pub struct SetVol; + +impl Command for SetVol { + type Response = (); + const COMMAND: &'static str = "setvol"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let volume = match parts.next() { + Some(s) => { + Volume::from_str(s).map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))? + } + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::SetVol(volume), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/playback_options/single.rs b/src/commands/playback_options/single.rs new file mode 100644 index 0000000..6ea42d5 --- /dev/null +++ b/src/commands/playback_options/single.rs @@ -0,0 +1,32 @@ +use std::{collections::HashMap, str::FromStr}; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +pub struct Single; + +impl Command for Single { + type Response = (); + const COMMAND: &'static str = "single"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let state = match parts.next() { + Some(s) => crate::common::BoolOrOneshot::from_str(s) + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::Single(state), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/playback_options/volume.rs b/src/commands/playback_options/volume.rs new file mode 100644 index 0000000..e559ce1 --- /dev/null +++ b/src/commands/playback_options/volume.rs @@ -0,0 +1,32 @@ +use std::{collections::HashMap, str::FromStr}; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseParserError, +}; + +struct Volume; + +impl Command for Volume { + type Response = (); + const COMMAND: &'static str = "volume"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let change = match parts.next() { + Some(s) => crate::request::Volume::from_str(s) + .map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?, + None => return Err(RequestParserError::UnexpectedEOF), + }; + + debug_assert!(parts.next().is_none()); + + Ok((Request::Volume(change), "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/querying_mpd_status.rs b/src/commands/querying_mpd_status.rs new file mode 100644 index 0000000..f369cdb --- /dev/null +++ b/src/commands/querying_mpd_status.rs @@ -0,0 +1,5 @@ +pub mod clearerror; +pub mod currentsong; +pub mod idle; +pub mod stats; +pub mod status; diff --git a/src/commands/querying_mpd_status/clearerror.rs b/src/commands/querying_mpd_status/clearerror.rs new file mode 100644 index 0000000..5f1b369 --- /dev/null +++ b/src/commands/querying_mpd_status/clearerror.rs @@ -0,0 +1,25 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +pub struct ClearError; + +impl Command for ClearError { + type Response = (); + const COMMAND: &'static str = "clearerror"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + + Ok((Request::ClearError, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/querying_mpd_status/currentsong.rs b/src/commands/querying_mpd_status/currentsong.rs new file mode 100644 index 0000000..460f6b7 --- /dev/null +++ b/src/commands/querying_mpd_status/currentsong.rs @@ -0,0 +1,26 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +pub struct CurrentSong; + +pub struct CurrentSongResponse {} + +impl Command for CurrentSong { + type Response = CurrentSongResponse; + const COMMAND: &'static str = "currentsong"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + + Ok((Request::CurrentSong, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + todo!() + } +} diff --git a/src/commands/querying_mpd_status/idle.rs b/src/commands/querying_mpd_status/idle.rs new file mode 100644 index 0000000..340fa40 --- /dev/null +++ b/src/commands/querying_mpd_status/idle.rs @@ -0,0 +1,33 @@ +use std::str::{FromStr, SplitWhitespace}; + +use crate::common::SubSystem; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +pub struct Idle; + +impl Command for Idle { + type Response = (); + const COMMAND: &'static str = "idle"; + + fn parse_request(mut parts: SplitWhitespace<'_>) -> RequestParserResult<'_> { + parts + .next() + .map_or(Ok((Request::Idle(None), "")), |subsystems| { + let subsystems = subsystems + .split(',') + .map(|subsystem| SubSystem::from_str(subsystem).unwrap()) + .collect(); + Ok((Request::Idle(Some(subsystems)), "")) + }) + } + + fn parse_response<'a>( + parts: std::collections::HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + debug_assert!(parts.is_empty()); + Ok(()) + } +} diff --git a/src/commands/querying_mpd_status/stats.rs b/src/commands/querying_mpd_status/stats.rs new file mode 100644 index 0000000..37bc7c6 --- /dev/null +++ b/src/commands/querying_mpd_status/stats.rs @@ -0,0 +1,37 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +pub struct Stats; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct StatsResponse { + pub uptime: u64, + pub playtime: u64, + pub artists: Option, + pub albums: Option, + pub songs: Option, + pub db_playtime: Option, + pub db_update: Option, +} + +impl Command for Stats { + type Response = StatsResponse; + const COMMAND: &'static str = "stats"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + + Ok((Request::Stats, "")) + } + + fn parse_response<'a>( + parts: HashMap<&'a str, GenericResponseValue<'a>>, + ) -> Result { + todo!() + } +} diff --git a/src/commands/querying_mpd_status/status.rs b/src/commands/querying_mpd_status/status.rs new file mode 100644 index 0000000..7498d3c --- /dev/null +++ b/src/commands/querying_mpd_status/status.rs @@ -0,0 +1,238 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +use crate::common::{Audio, BoolOrOneshot, SongId, SongPosition}; + +use crate::commands::{ + get_and_parse_optional_property, get_and_parse_property, get_optional_property, get_property, + Command, GenericResponseValue, Request, RequestParserResult, ResponseParserError, +}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum StatusResponseState { + Play, + Stop, + Pause, +} + +impl FromStr for StatusResponseState { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "play" => Ok(StatusResponseState::Play), + "stop" => Ok(StatusResponseState::Stop), + "pause" => Ok(StatusResponseState::Pause), + _ => Err(()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct StatusResponse { + pub partition: String, + // Note: the Option<>::None here is serialized as -1 + pub volume: Option, + pub repeat: bool, + pub random: bool, + pub single: BoolOrOneshot, + pub consume: BoolOrOneshot, + pub playlist: u32, + pub playlist_length: u64, + pub state: StatusResponseState, + pub song: Option, + pub song_id: Option, + pub next_song: Option, + pub next_song_id: Option, + pub time: Option<(u64, u64)>, + pub elapsed: Option, + pub duration: Option, + pub bitrate: Option, + pub xfade: Option, + pub mixrampdb: Option, + pub mixrampdelay: Option, + pub audio: Option