diff --git a/src/commands.rs b/src/commands.rs index e0c94ab..860d9d3 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -16,6 +16,7 @@ mod playback_options; mod querying_mpd_status; mod queue; mod reflection; +mod stickers; mod stored_playlists; pub use audio_output_devices::*; @@ -29,6 +30,7 @@ pub use playback_options::*; pub use querying_mpd_status::*; pub use queue::*; pub use reflection::*; +pub use stickers::*; pub use stored_playlists::*; pub trait Command { diff --git a/src/commands/stickers.rs b/src/commands/stickers.rs new file mode 100644 index 0000000..840d65b --- /dev/null +++ b/src/commands/stickers.rs @@ -0,0 +1,21 @@ +pub mod sticker_dec; +pub mod sticker_delete; +pub mod sticker_find; +pub mod sticker_get; +pub mod sticker_inc; +pub mod sticker_list; +pub mod sticker_set; +pub mod stickernames; +pub mod stickernamestypes; +pub mod stickertypes; + +pub use sticker_dec::StickerDec; +pub use sticker_delete::StickerDelete; +pub use sticker_find::StickerFind; +pub use sticker_get::StickerGet; +pub use sticker_inc::StickerInc; +pub use sticker_list::StickerList; +pub use sticker_set::StickerSet; +pub use stickernames::StickerNames; +pub use stickernamestypes::StickerNamesTypes; +pub use stickertypes::StickerTypes; diff --git a/src/commands/stickers/sticker_dec.rs b/src/commands/stickers/sticker_dec.rs new file mode 100644 index 0000000..96565b3 --- /dev/null +++ b/src/commands/stickers/sticker_dec.rs @@ -0,0 +1,45 @@ +use crate::commands::{ + Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, + ResponseParserError, +}; + +pub struct StickerDec; + +impl Command for StickerDec { + type Response = (); + const COMMAND: &'static str = "sticker dec"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let sticker_type = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let sticker_type = sticker_type + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, sticker_type.to_owned()))?; + + let uri = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let uri = uri + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, uri.to_owned()))?; + + let name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let name = name + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, name.to_owned()))?; + + let value = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let value = value + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, value.to_owned()))?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::StickerDec(sticker_type, uri, name, value), "")) + } + + fn parse_response( + parts: ResponseAttributes<'_>, + ) -> Result { + debug_assert!(parts.is_empty()); + + Ok(()) + } +} diff --git a/src/commands/stickers/sticker_delete.rs b/src/commands/stickers/sticker_delete.rs new file mode 100644 index 0000000..42e79d5 --- /dev/null +++ b/src/commands/stickers/sticker_delete.rs @@ -0,0 +1,40 @@ +use crate::commands::{ + Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, + ResponseParserError, +}; + +pub struct StickerDelete; + +impl Command for StickerDelete { + type Response = (); + const COMMAND: &'static str = "sticker delete"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let sticker_type = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let sticker_type = sticker_type + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, sticker_type.to_owned()))?; + + let uri = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let uri = uri + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, uri.to_owned()))?; + + let name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let name = name + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, name.to_owned()))?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::StickerDelete(sticker_type, uri, name), "")) + } + + fn parse_response( + parts: ResponseAttributes<'_>, + ) -> Result { + debug_assert!(parts.is_empty()); + + Ok(()) + } +} diff --git a/src/commands/stickers/sticker_find.rs b/src/commands/stickers/sticker_find.rs new file mode 100644 index 0000000..1128136 --- /dev/null +++ b/src/commands/stickers/sticker_find.rs @@ -0,0 +1,120 @@ +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseAttributes, ResponseParserError, +}; + +pub struct StickerFind; + +pub struct StickerFindResponseEntry { + pub uri: String, + pub name: String, + pub value: String, +} + +pub type StickerFindResponse = Vec; + +impl Command for StickerFind { + type Response = StickerFindResponse; + const COMMAND: &'static str = "sticker find"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let sticker_type = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let sticker_type = sticker_type + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, sticker_type.to_owned()))?; + + let uri = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let uri = uri + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, uri.to_owned()))?; + + let name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let name = name + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, name.to_owned()))?; + + // TODO: handle the case for this command as well: + // sticker find {TYPE} {URI} {NAME} = {VALUE} [sort {SORTTYPE}] [window {START:END}] + + let mut sort_or_window = parts.next(); + let mut sort = None; + if let Some("sort") = sort_or_window { + sort = Some( + parts + .next() + .ok_or(RequestParserError::UnexpectedEOF)? + .to_string(), + ); + sort_or_window = parts.next(); + } + + let mut window = None; + if let Some("window") = sort_or_window { + let w = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + window = Some( + w.parse() + .map_err(|_| RequestParserError::SyntaxError(0, w.to_string()))?, + ); + } + + debug_assert!(parts.next().is_none()); + + Ok(( + Request::StickerFind(sticker_type, uri, name, sort, window), + "", + )) + } + + fn parse_response( + parts: ResponseAttributes<'_>, + ) -> Result { + let parts: Vec<_> = parts.into(); + let mut stickers = Vec::new(); + + for sticker_uri_pair in parts.chunks_exact(2) { + // TODO: don't assume that the order of the properties is fixed + let uri = sticker_uri_pair + .first() + .ok_or(ResponseParserError::UnexpectedEOF)?; + let sticker = sticker_uri_pair + .get(1) + .ok_or(ResponseParserError::UnexpectedEOF)?; + + debug_assert!(sticker.0 == "sticker"); + // TODO: debug assert that this is a valid sticker type + // debug_assert!(uri.0 == ""); + + let uri = match uri.1 { + GenericResponseValue::Text(s) => s.to_string(), + GenericResponseValue::Binary(_) => { + return Err(ResponseParserError::UnexpectedPropertyType(uri.0, "Binary")) + } + }; + + let sticker = match sticker.1 { + GenericResponseValue::Text(s) => s, + GenericResponseValue::Binary(_) => { + return Err(ResponseParserError::UnexpectedPropertyType( + "sticker", "Binary", + )) + } + }; + + // TODO: This assumes the first = is the only one. + // See: https://github.com/MusicPlayerDaemon/MPD/issues/2166 + let mut split = sticker.split("="); + let name = split + .next() + .ok_or(ResponseParserError::SyntaxError(0, sticker))? + .to_string(); + let value = split + .next() + .ok_or(ResponseParserError::SyntaxError(1, sticker))? + .to_string(); + + stickers.push(StickerFindResponseEntry { uri, name, value }); + } + + Ok(stickers) + } +} diff --git a/src/commands/stickers/sticker_get.rs b/src/commands/stickers/sticker_get.rs new file mode 100644 index 0000000..4421f47 --- /dev/null +++ b/src/commands/stickers/sticker_get.rs @@ -0,0 +1,57 @@ +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseAttributes, ResponseParserError, +}; + +pub struct StickerGet; + +pub type StickerGetResponse = String; + +impl Command for StickerGet { + type Response = StickerGetResponse; + const COMMAND: &'static str = "sticker get"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let sticker_type = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let sticker_type = sticker_type + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, sticker_type.to_owned()))?; + + let uri = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let uri = uri + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, uri.to_owned()))?; + + let name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let name = name + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, name.to_owned()))?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::StickerGet(sticker_type, uri, name), "")) + } + + fn parse_response( + parts: ResponseAttributes<'_>, + ) -> Result { + let parts: Vec<_> = parts.into(); + let mut parts = parts.into_iter(); + let sticker = parts.next().ok_or(ResponseParserError::UnexpectedEOF)?; + + debug_assert!(parts.next().is_none()); + + debug_assert!(sticker.0 == "sticker"); + + let sticker = match sticker.1 { + GenericResponseValue::Text(s) => s.to_string(), + GenericResponseValue::Binary(_) => { + return Err(ResponseParserError::UnexpectedPropertyType( + "sticker", "Binary", + )) + } + }; + + Ok(sticker) + } +} diff --git a/src/commands/stickers/sticker_inc.rs b/src/commands/stickers/sticker_inc.rs new file mode 100644 index 0000000..186b4a7 --- /dev/null +++ b/src/commands/stickers/sticker_inc.rs @@ -0,0 +1,45 @@ +use crate::commands::{ + Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, + ResponseParserError, +}; + +pub struct StickerInc; + +impl Command for StickerInc { + type Response = (); + const COMMAND: &'static str = "sticker inc"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let sticker_type = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let sticker_type = sticker_type + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, sticker_type.to_owned()))?; + + let uri = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let uri = uri + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, uri.to_owned()))?; + + let name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let name = name + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, name.to_owned()))?; + + let value = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let value = value + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, value.to_owned()))?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::StickerInc(sticker_type, uri, name, value), "")) + } + + fn parse_response( + parts: ResponseAttributes<'_>, + ) -> Result { + debug_assert!(parts.is_empty()); + + Ok(()) + } +} diff --git a/src/commands/stickers/sticker_list.rs b/src/commands/stickers/sticker_list.rs new file mode 100644 index 0000000..66add21 --- /dev/null +++ b/src/commands/stickers/sticker_list.rs @@ -0,0 +1,60 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserError, RequestParserResult, + ResponseAttributes, ResponseParserError, +}; + +pub struct StickerList; + +pub type StickerListResponse = HashMap; + +impl Command for StickerList { + type Response = StickerListResponse; + const COMMAND: &'static str = "sticker list"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let sticker_type = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let sticker_type = sticker_type + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, "sticker_type".to_owned()))?; + + let uri = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let uri = uri + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, "uri".to_owned()))?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::StickerList(sticker_type, uri), "")) + } + + fn parse_response( + parts: ResponseAttributes<'_>, + ) -> Result { + let parts: Vec<_> = parts.into(); + debug_assert!(parts.iter().all(|(k, _)| *k == "sticker")); + + let result = parts + .iter() + .map(|(_, v)| match v { + GenericResponseValue::Text(value) => Ok(value), + GenericResponseValue::Binary(_) => Err( + ResponseParserError::UnexpectedPropertyType("sticker", "Binary"), + ), + }) + .collect::, ResponseParserError>>()?; + + result + .into_iter() + .map(|v| { + // TODO: This assumes the first = is the only one. + // See: https://github.com/MusicPlayerDaemon/MPD/issues/2166 + let mut split = v.split('='); + let key = split.next().ok_or(ResponseParserError::SyntaxError(0, v))?; + let value = split.next().ok_or(ResponseParserError::SyntaxError(1, v))?; + Ok((key.to_string(), value.to_string())) + }) + .collect::, ResponseParserError>>() + } +} diff --git a/src/commands/stickers/sticker_set.rs b/src/commands/stickers/sticker_set.rs new file mode 100644 index 0000000..7f7bf13 --- /dev/null +++ b/src/commands/stickers/sticker_set.rs @@ -0,0 +1,45 @@ +use crate::commands::{ + Command, Request, RequestParserError, RequestParserResult, ResponseAttributes, + ResponseParserError, +}; + +pub struct StickerSet; + +impl Command for StickerSet { + type Response = (); + const COMMAND: &'static str = "sticker set"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let sticker_type = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let sticker_type = sticker_type + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, sticker_type.to_owned()))?; + + let uri = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let uri = uri + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, uri.to_owned()))?; + + let name = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let name = name + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, name.to_owned()))?; + + let value = parts.next().ok_or(RequestParserError::UnexpectedEOF)?; + let value = value + .parse() + .map_err(|_| RequestParserError::SyntaxError(1, value.to_owned()))?; + + debug_assert!(parts.next().is_none()); + + Ok((Request::StickerSet(sticker_type, uri, name, value), "")) + } + + fn parse_response( + parts: ResponseAttributes<'_>, + ) -> Result { + debug_assert!(parts.is_empty()); + + Ok(()) + } +} diff --git a/src/commands/stickers/stickernames.rs b/src/commands/stickers/stickernames.rs new file mode 100644 index 0000000..838e3bd --- /dev/null +++ b/src/commands/stickers/stickernames.rs @@ -0,0 +1,38 @@ +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseAttributes, + ResponseParserError, +}; + +pub struct StickerNames; + +pub type StickerNamesResponse = Vec; + +impl Command for StickerNames { + type Response = StickerNamesResponse; + const COMMAND: &'static str = "stickernames"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + + Ok((Request::StickerNames, "")) + } + + fn parse_response( + parts: ResponseAttributes<'_>, + ) -> Result { + debug_assert!(parts.0.iter().all(|(k, _)| *k == "name")); + + let list = parts + .0 + .iter() + .map(|(_, v)| match v { + GenericResponseValue::Text(value) => Ok(value.to_string()), + GenericResponseValue::Binary(_) => Err( + ResponseParserError::UnexpectedPropertyType("name", "Binary"), + ), + }) + .collect::, ResponseParserError>>()?; + + Ok(list) + } +} diff --git a/src/commands/stickers/stickernamestypes.rs b/src/commands/stickers/stickernamestypes.rs new file mode 100644 index 0000000..e39b9c1 --- /dev/null +++ b/src/commands/stickers/stickernamestypes.rs @@ -0,0 +1,63 @@ +use std::collections::HashMap; + +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseAttributes, + ResponseParserError, +}; + +pub struct StickerNamesTypes; + +pub type StickerNamesTypesResponse = HashMap>; + +impl Command for StickerNamesTypes { + type Response = StickerNamesTypesResponse; + const COMMAND: &'static str = "stickernamestypes"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + let sticker_type = parts.next().map(|s| s.to_string()); + + debug_assert!(parts.next().is_none()); + + Ok((Request::StickerNamesTypes(sticker_type), "")) + } + + fn parse_response( + parts: ResponseAttributes<'_>, + ) -> Result { + let parts: Vec<_> = parts.into(); + let mut result = HashMap::new(); + + for name_type_pair in parts.chunks_exact(2) { + // TODO: don't depend on order, just make sure we have both + let (name_key, name_value) = name_type_pair[0]; + let (type_key, type_value) = name_type_pair[1]; + debug_assert!(name_key == "name"); + debug_assert!(type_key == "type"); + + let name = match name_value { + GenericResponseValue::Text(s) => s.to_string(), + GenericResponseValue::Binary(_) => { + return Err(ResponseParserError::UnexpectedPropertyType( + "name", "Binary", + )) + } + }; + + let sticker_type = match type_value { + GenericResponseValue::Text(s) => s.to_string(), + GenericResponseValue::Binary(_) => { + return Err(ResponseParserError::UnexpectedPropertyType( + "type", "Binary", + )) + } + }; + + result + .entry(name) + .or_insert_with(Vec::new) + .push(sticker_type); + } + + Ok(result) + } +} diff --git a/src/commands/stickers/stickertypes.rs b/src/commands/stickers/stickertypes.rs new file mode 100644 index 0000000..1f8bc2e --- /dev/null +++ b/src/commands/stickers/stickertypes.rs @@ -0,0 +1,38 @@ +use crate::commands::{ + Command, GenericResponseValue, Request, RequestParserResult, ResponseAttributes, + ResponseParserError, +}; + +pub struct StickerTypes; + +pub type StickerTypesResponse = Vec; + +impl Command for StickerTypes { + type Response = StickerTypesResponse; + const COMMAND: &'static str = "stickertypes"; + + fn parse_request(mut parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> { + debug_assert!(parts.next().is_none()); + + Ok((Request::StickerTypes, "")) + } + + fn parse_response( + parts: ResponseAttributes<'_>, + ) -> Result { + debug_assert!(parts.0.iter().all(|(k, _)| *k == "stickertype")); + + let list = parts + .0 + .iter() + .map(|(_, v)| match v { + GenericResponseValue::Text(value) => Ok(value.to_string()), + GenericResponseValue::Binary(_) => Err( + ResponseParserError::UnexpectedPropertyType("stickertype", "Binary"), + ), + }) + .collect::, ResponseParserError>>()?; + + Ok(list) + } +} diff --git a/src/request.rs b/src/request.rs index 8966707..c0c4a0f 100644 --- a/src/request.rs +++ b/src/request.rs @@ -139,6 +139,8 @@ pub enum Request { // -- Sticker Commands -- // StickerGet(StickerType, Uri, String), StickerSet(StickerType, Uri, String, String), + StickerInc(StickerType, Uri, String, usize), + StickerDec(StickerType, Uri, String, usize), StickerDelete(StickerType, Uri, String), StickerList(StickerType, Uri), StickerFind(StickerType, Uri, String, Option, Option), @@ -475,6 +477,16 @@ impl Request { ListNeighbors::COMMAND => ListNeighbors::parse_request(parts), /* stickers */ + StickerGet::COMMAND => StickerGet::parse_request(parts), + StickerSet::COMMAND => StickerSet::parse_request(parts), + StickerInc::COMMAND => StickerInc::parse_request(parts), + StickerDec::COMMAND => StickerDec::parse_request(parts), + StickerDelete::COMMAND => StickerDelete::parse_request(parts), + StickerList::COMMAND => StickerList::parse_request(parts), + StickerFind::COMMAND => StickerFind::parse_request(parts), + StickerNames::COMMAND => StickerNames::parse_request(parts), + StickerTypes::COMMAND => StickerTypes::parse_request(parts), + StickerNamesTypes::COMMAND => StickerNamesTypes::parse_request(parts), /* connection settings */ Close::COMMAND => Close::parse_request(parts),