Continued development
This commit is contained in:
parent
8293c6e6e5
commit
49e070a41d
30
Cargo.lock
generated
30
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
196
src/commands.rs
Normal file
196
src/commands.rs
Normal file
@ -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<Self::Response, ResponseParserError<'a>>;
|
||||
|
||||
fn parse_raw_response(raw: &str) -> Result<Self::Response, ResponseParserError<'_>> {
|
||||
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<GenericResponse<'a>, &'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<GenericResponseValue<'a>>),
|
||||
}
|
||||
|
||||
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;
|
5
src/commands/audio_output_devices.rs
Normal file
5
src/commands/audio_output_devices.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod disableoutput;
|
||||
pub mod enableoutput;
|
||||
pub mod outputs;
|
||||
pub mod outputset;
|
||||
pub mod toggleoutput;
|
28
src/commands/audio_output_devices/disableoutput.rs
Normal file
28
src/commands/audio_output_devices/disableoutput.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
28
src/commands/audio_output_devices/enableoutput.rs
Normal file
28
src/commands/audio_output_devices/enableoutput.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
36
src/commands/audio_output_devices/outputs.rs
Normal file
36
src/commands/audio_output_devices/outputs.rs
Normal file
@ -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<String, String>,
|
||||
}
|
||||
|
||||
pub type OutputsResponse = Vec<Output>;
|
||||
|
||||
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<Self::Response, ResponseParserError> {
|
||||
todo!()
|
||||
// debug_assert!(parts.is_empty());
|
||||
// Ok(())
|
||||
}
|
||||
}
|
37
src/commands/audio_output_devices/outputset.rs
Normal file
37
src/commands/audio_output_devices/outputset.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
28
src/commands/audio_output_devices/toggleoutput.rs
Normal file
28
src/commands/audio_output_devices/toggleoutput.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
5
src/commands/client_to_client.rs
Normal file
5
src/commands/client_to_client.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod channels;
|
||||
pub mod readmessages;
|
||||
pub mod sendmessage;
|
||||
pub mod subscribe;
|
||||
pub mod unsubscribe;
|
40
src/commands/client_to_client/channels.rs
Normal file
40
src/commands/client_to_client/channels.rs
Normal file
@ -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<String>,
|
||||
}
|
||||
|
||||
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<Self::Response, ResponseParserError> {
|
||||
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::<Option<Vec<_>>>()
|
||||
// .ok_or(ResponseParserError::UnexpectedType("channels", "text"))?;
|
||||
|
||||
// Ok(ChannelsResponse { channels })
|
||||
}
|
||||
}
|
29
src/commands/client_to_client/readmessages.rs
Normal file
29
src/commands/client_to_client/readmessages.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
31
src/commands/client_to_client/sendmessage.rs
Normal file
31
src/commands/client_to_client/sendmessage.rs
Normal file
@ -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::<Vec<_>>().join(" ");
|
||||
|
||||
debug_assert!(!message.is_empty());
|
||||
|
||||
Ok((Request::SendMessage(channel.to_string(), message), ""))
|
||||
}
|
||||
|
||||
fn parse_response<'a>(
|
||||
parts: HashMap<&'a str, GenericResponseValue<'a>>,
|
||||
) -> Result<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
28
src/commands/client_to_client/subscribe.rs
Normal file
28
src/commands/client_to_client/subscribe.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
28
src/commands/client_to_client/unsubscribe.rs
Normal file
28
src/commands/client_to_client/unsubscribe.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
9
src/commands/controlling_playback.rs
Normal file
9
src/commands/controlling_playback.rs
Normal file
@ -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;
|
24
src/commands/controlling_playback/next.rs
Normal file
24
src/commands/controlling_playback/next.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
29
src/commands/controlling_playback/pause.rs
Normal file
29
src/commands/controlling_playback/pause.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
36
src/commands/controlling_playback/play.rs
Normal file
36
src/commands/controlling_playback/play.rs
Normal file
@ -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::<SongPosition>()
|
||||
.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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
36
src/commands/controlling_playback/playid.rs
Normal file
36
src/commands/controlling_playback/playid.rs
Normal file
@ -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::<SongId>()
|
||||
.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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
24
src/commands/controlling_playback/previous.rs
Normal file
24
src/commands/controlling_playback/previous.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
41
src/commands/controlling_playback/seek.rs
Normal file
41
src/commands/controlling_playback/seek.rs
Normal file
@ -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::<SongPosition>()
|
||||
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
|
||||
None => return Err(RequestParserError::UnexpectedEOF),
|
||||
};
|
||||
|
||||
let time = match parts.next() {
|
||||
Some(t) => t
|
||||
.parse::<TimeWithFractions>()
|
||||
.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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
54
src/commands/controlling_playback/seekcur.rs
Normal file
54
src/commands/controlling_playback/seekcur.rs
Normal file
@ -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::<TimeWithFractions>()
|
||||
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
|
||||
),
|
||||
t if t.starts_with('-') => (
|
||||
SeekMode::Relative,
|
||||
-t[1..]
|
||||
.parse::<TimeWithFractions>()
|
||||
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
|
||||
),
|
||||
t => (
|
||||
SeekMode::Absolute,
|
||||
t.parse::<TimeWithFractions>()
|
||||
.map_err(|_| RequestParserError::SyntaxError(0, t.to_owned()))?,
|
||||
),
|
||||
};
|
||||
|
||||
Ok((Request::SeekCur(mode, time), ""))
|
||||
}
|
||||
|
||||
fn parse_response<'a>(
|
||||
parts: HashMap<&'a str, GenericResponseValue<'a>>,
|
||||
) -> Result<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
41
src/commands/controlling_playback/seekid.rs
Normal file
41
src/commands/controlling_playback/seekid.rs
Normal file
@ -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::<SongId>()
|
||||
.map_err(|_| RequestParserError::SyntaxError(0, s.to_owned()))?,
|
||||
None => return Err(RequestParserError::UnexpectedEOF),
|
||||
};
|
||||
|
||||
let time = match parts.next() {
|
||||
Some(t) => t
|
||||
.parse::<TimeWithFractions>()
|
||||
.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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
24
src/commands/controlling_playback/stop.rs
Normal file
24
src/commands/controlling_playback/stop.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
12
src/commands/playback_options.rs
Normal file
12
src/commands/playback_options.rs
Normal file
@ -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;
|
32
src/commands/playback_options/consume.rs
Normal file
32
src/commands/playback_options/consume.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
36
src/commands/playback_options/crossfade.rs
Normal file
36
src/commands/playback_options/crossfade.rs
Normal file
@ -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::<Seconds>()
|
||||
.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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
31
src/commands/playback_options/getvol.rs
Normal file
31
src/commands/playback_options/getvol.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
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)
|
||||
}
|
||||
}
|
33
src/commands/playback_options/mixrampdb.rs
Normal file
33
src/commands/playback_options/mixrampdb.rs
Normal file
@ -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::<f32>()
|
||||
.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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
36
src/commands/playback_options/mixrampdelay.rs
Normal file
36
src/commands/playback_options/mixrampdelay.rs
Normal file
@ -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::<Seconds>()
|
||||
.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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
33
src/commands/playback_options/random.rs
Normal file
33
src/commands/playback_options/random.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
33
src/commands/playback_options/repeat.rs
Normal file
33
src/commands/playback_options/repeat.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
35
src/commands/playback_options/replay_gain_mode.rs
Normal file
35
src/commands/playback_options/replay_gain_mode.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
37
src/commands/playback_options/replay_gain_status.rs
Normal file
37
src/commands/playback_options/replay_gain_status.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
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)
|
||||
})?,
|
||||
})
|
||||
}
|
||||
}
|
36
src/commands/playback_options/setvol.rs
Normal file
36
src/commands/playback_options/setvol.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
32
src/commands/playback_options/single.rs
Normal file
32
src/commands/playback_options/single.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
32
src/commands/playback_options/volume.rs
Normal file
32
src/commands/playback_options/volume.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
5
src/commands/querying_mpd_status.rs
Normal file
5
src/commands/querying_mpd_status.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod clearerror;
|
||||
pub mod currentsong;
|
||||
pub mod idle;
|
||||
pub mod stats;
|
||||
pub mod status;
|
25
src/commands/querying_mpd_status/clearerror.rs
Normal file
25
src/commands/querying_mpd_status/clearerror.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
26
src/commands/querying_mpd_status/currentsong.rs
Normal file
26
src/commands/querying_mpd_status/currentsong.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
33
src/commands/querying_mpd_status/idle.rs
Normal file
33
src/commands/querying_mpd_status/idle.rs
Normal file
@ -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<Self::Response, ResponseParserError> {
|
||||
debug_assert!(parts.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
}
|
37
src/commands/querying_mpd_status/stats.rs
Normal file
37
src/commands/querying_mpd_status/stats.rs
Normal file
@ -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<u64>,
|
||||
pub albums: Option<u64>,
|
||||
pub songs: Option<u64>,
|
||||
pub db_playtime: Option<u64>,
|
||||
pub db_update: Option<u64>,
|
||||
}
|
||||
|
||||
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<Self::Response, ResponseParserError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
238
src/commands/querying_mpd_status/status.rs
Normal file
238
src/commands/querying_mpd_status/status.rs
Normal file
@ -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<Self, Self::Err> {
|
||||
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<u8>,
|
||||
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<SongPosition>,
|
||||
pub song_id: Option<SongId>,
|
||||
pub next_song: Option<SongPosition>,
|
||||
pub next_song_id: Option<SongId>,
|
||||
pub time: Option<(u64, u64)>,
|
||||
pub elapsed: Option<f64>,
|
||||
pub duration: Option<f64>,
|
||||
pub bitrate: Option<u32>,
|
||||
pub xfade: Option<u32>,
|
||||
pub mixrampdb: Option<f64>,
|
||||
pub mixrampdelay: Option<f64>,
|
||||
pub audio: Option<Audio>,
|
||||
pub updating_db: Option<u64>,
|
||||
pub error: Option<String>,
|
||||
pub last_loaded_playlist: Option<String>,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_status_response<'a>(
|
||||
parts: std::collections::HashMap<&'a str, GenericResponseValue<'a>>,
|
||||
) -> Result<StatusResponse, ResponseParserError> {
|
||||
let partition = get_property!(parts, "partition", Text).to_string();
|
||||
|
||||
let volume = match get_property!(parts, "volume", Text) {
|
||||
"-1" => None,
|
||||
volume => Some(
|
||||
volume
|
||||
.parse()
|
||||
.map_err(|_| ResponseParserError::InvalidProperty("volume", volume))?,
|
||||
),
|
||||
};
|
||||
|
||||
let repeat = match get_property!(parts, "repeat", Text) {
|
||||
"0" => Ok(false),
|
||||
"1" => Ok(true),
|
||||
repeat => Err(ResponseParserError::InvalidProperty("repeat", repeat)),
|
||||
}?;
|
||||
|
||||
let random = match get_property!(parts, "random", Text) {
|
||||
"0" => Ok(false),
|
||||
"1" => Ok(true),
|
||||
random => Err(ResponseParserError::InvalidProperty("random", random)),
|
||||
}?;
|
||||
|
||||
let single = get_and_parse_property!(parts, "single", Text);
|
||||
let consume = get_and_parse_property!(parts, "consume", Text);
|
||||
let playlist: u32 = get_and_parse_property!(parts, "playlist", Text);
|
||||
let playlist_length: u64 = get_and_parse_property!(parts, "playlistlength", Text);
|
||||
let state: StatusResponseState = get_and_parse_property!(parts, "state", Text);
|
||||
let song: Option<SongPosition> = get_and_parse_optional_property!(parts, "song", Text);
|
||||
let song_id: Option<SongId> = get_and_parse_optional_property!(parts, "songid", Text);
|
||||
let next_song: Option<SongPosition> = get_and_parse_optional_property!(parts, "nextsong", Text);
|
||||
let next_song_id: Option<SongId> = get_and_parse_optional_property!(parts, "nextsongid", Text);
|
||||
|
||||
let time = match get_optional_property!(parts, "time", Text) {
|
||||
Some(time) => {
|
||||
let mut parts = time.split(':');
|
||||
let elapsed = parts
|
||||
.next()
|
||||
.ok_or(ResponseParserError::SyntaxError(0, time))?
|
||||
.parse()
|
||||
.map_err(|_| ResponseParserError::InvalidProperty("time", time))?;
|
||||
let duration = parts
|
||||
.next()
|
||||
.ok_or(ResponseParserError::SyntaxError(0, time))?
|
||||
.parse()
|
||||
.map_err(|_| ResponseParserError::InvalidProperty("time", time))?;
|
||||
Some((elapsed, duration))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let elapsed = get_and_parse_optional_property!(parts, "elapsed", Text);
|
||||
let duration = get_and_parse_optional_property!(parts, "duration", Text);
|
||||
let bitrate = get_and_parse_optional_property!(parts, "bitrate", Text);
|
||||
let xfade = get_and_parse_optional_property!(parts, "xfade", Text);
|
||||
let mixrampdb = get_and_parse_optional_property!(parts, "mixrampdb", Text);
|
||||
let mixrampdelay = get_and_parse_optional_property!(parts, "mixrampdelay", Text);
|
||||
let audio = get_and_parse_optional_property!(parts, "audio", Text);
|
||||
let updating_db = get_and_parse_optional_property!(parts, "updating_db", Text);
|
||||
let error = get_and_parse_optional_property!(parts, "error", Text);
|
||||
let last_loaded_playlist =
|
||||
get_and_parse_optional_property!(parts, "last_loaded_playlist", Text);
|
||||
|
||||
Ok(StatusResponse {
|
||||
partition,
|
||||
volume,
|
||||
repeat,
|
||||
random,
|
||||
single,
|
||||
consume,
|
||||
playlist,
|
||||
playlist_length,
|
||||
state,
|
||||
song,
|
||||
song_id,
|
||||
next_song,
|
||||
next_song_id,
|
||||
time,
|
||||
elapsed,
|
||||
duration,
|
||||
bitrate,
|
||||
xfade,
|
||||
mixrampdb,
|
||||
mixrampdelay,
|
||||
audio,
|
||||
updating_db,
|
||||
error,
|
||||
last_loaded_playlist,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Status;
|
||||
|
||||
impl Command for Status {
|
||||
type Response = StatusResponse;
|
||||
const COMMAND: &'static str = "status";
|
||||
|
||||
fn parse_request(_parts: std::str::SplitWhitespace<'_>) -> RequestParserResult<'_> {
|
||||
Ok((Request::Status, ""))
|
||||
}
|
||||
|
||||
fn parse_response<'a>(
|
||||
parts: std::collections::HashMap<&'a str, GenericResponseValue<'a>>,
|
||||
) -> Result<Self::Response, ResponseParserError> {
|
||||
parse_status_response(parts)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parse_status_response() {
|
||||
let contents = indoc! { r#"
|
||||
volume: 66
|
||||
repeat: 1
|
||||
random: 1
|
||||
single: 0
|
||||
consume: 0
|
||||
partition: default
|
||||
playlist: 2
|
||||
playlistlength: 78
|
||||
mixrampdb: 0
|
||||
state: play
|
||||
song: 0
|
||||
songid: 1
|
||||
time: 225:263
|
||||
elapsed: 225.376
|
||||
bitrate: 127
|
||||
duration: 262.525
|
||||
audio: 44100:f:2
|
||||
nextsong: 44
|
||||
nextsongid: 45
|
||||
OK
|
||||
"# };
|
||||
|
||||
assert_eq!(
|
||||
Status::parse_raw_response(contents),
|
||||
Ok(StatusResponse {
|
||||
partition: "default".into(),
|
||||
volume: Some(66),
|
||||
repeat: true,
|
||||
random: true,
|
||||
single: BoolOrOneshot::False,
|
||||
consume: BoolOrOneshot::False,
|
||||
playlist: 2,
|
||||
playlist_length: 78,
|
||||
state: StatusResponseState::Play,
|
||||
song: Some(0),
|
||||
song_id: Some(1),
|
||||
next_song: Some(44),
|
||||
next_song_id: Some(45),
|
||||
time: Some((225, 263)),
|
||||
elapsed: Some(225.376),
|
||||
duration: Some(262.525),
|
||||
bitrate: Some(127),
|
||||
xfade: None,
|
||||
mixrampdb: Some(0.0),
|
||||
mixrampdelay: None,
|
||||
audio: Some(Audio {
|
||||
sample_rate: 44100,
|
||||
bits: 16,
|
||||
channels: 2,
|
||||
}),
|
||||
updating_db: None,
|
||||
error: None,
|
||||
last_loaded_playlist: None,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
140
src/common.rs
140
src/common.rs
@ -1,6 +1,33 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum Tag {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type SongPosition = u32;
|
||||
pub type SongId = u32;
|
||||
pub type Seconds = u32;
|
||||
pub type TimeWithFractions = f64;
|
||||
pub type OneOrRange = (SongPosition, Option<SongPosition>);
|
||||
pub type WindowRange = (Option<SongPosition>, Option<SongPosition>);
|
||||
pub type Priority = u8;
|
||||
pub type PlaylistName = String;
|
||||
pub type Offset = u32;
|
||||
|
||||
// TODO: use a proper types
|
||||
pub type TagName = String;
|
||||
pub type TagValue = String;
|
||||
pub type Uri = String;
|
||||
pub type Path = String;
|
||||
pub type Filter = String;
|
||||
pub type Sort = String;
|
||||
pub type Version = String;
|
||||
pub type Feature = String;
|
||||
pub type PartitionName = String;
|
||||
pub type AudioOutputId = String;
|
||||
pub type ChannelName = String;
|
||||
pub type StickerType = String;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Tag {
|
||||
/// The artist name. Its meaning is not well-defined; see “composer” and “performer” for more specific tags.
|
||||
Artist(String),
|
||||
/// Same as artist, but for sorting. This usually omits prefixes such as “The”.
|
||||
@ -25,9 +52,9 @@ pub(crate) enum Tag {
|
||||
Genre(String),
|
||||
/// The mood of the audio with a few keywords.
|
||||
Mood(String),
|
||||
/// The song’s release date. This is usually a 4-digit year.
|
||||
/// The song's release date. This is usually a 4-digit year.
|
||||
Date(String),
|
||||
/// The song’s original release date.
|
||||
/// The song's original release date.
|
||||
OriginalDate(String),
|
||||
/// The artist who composed the song.
|
||||
Composer(String),
|
||||
@ -75,3 +102,108 @@ pub(crate) enum Tag {
|
||||
/// Other tags not covered by the above
|
||||
Other(String, String),
|
||||
}
|
||||
|
||||
/// These are different parts of the canonical MPD server.
|
||||
/// They are mostly used in the protocol with the `idle` command,
|
||||
/// signalling that the client is waiting for changes in any, one or multiple of these subsystems.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SubSystem {
|
||||
/// The song database has been modified after update.
|
||||
Database,
|
||||
/// A database update has started or finished. If the database was modified during the update, the database event is also emitted.
|
||||
Update,
|
||||
/// A stored playlist has been modified, renamed, created or deleted
|
||||
StoredPlaylist,
|
||||
/// The queue (i.e. the current playlist) has been modified
|
||||
Playlist,
|
||||
/// The player has been started, stopped or seeked or tags of the currently playing song have changed (e.g. received from stream)
|
||||
Player,
|
||||
/// The volume has been changed
|
||||
Mixer,
|
||||
/// An audio output has been added, removed or modified (e.g. renamed, enabled or disabled)
|
||||
Output,
|
||||
/// Options like repeat, random, crossfade, replay gain
|
||||
Options,
|
||||
/// A partition was added, removed or changed
|
||||
Partition,
|
||||
/// The sticker database has been modified.
|
||||
Sticker,
|
||||
/// A client has subscribed or unsubscribed to a channel
|
||||
Subscription,
|
||||
/// A message was received on a channel this client is subscribed to; this event is only emitted when the client’s message queue is empty
|
||||
Message,
|
||||
/// A neighbor was found or lost
|
||||
Neighbor,
|
||||
/// The mount list has changed
|
||||
Mount,
|
||||
|
||||
/// Other subsystems not covered by the above
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl FromStr for SubSystem {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"database" => Self::Database,
|
||||
"update" => Self::Update,
|
||||
"stored_playlist" => Self::StoredPlaylist,
|
||||
"playlist" => Self::Playlist,
|
||||
"player" => Self::Player,
|
||||
"mixer" => Self::Mixer,
|
||||
"output" => Self::Output,
|
||||
"options" => Self::Options,
|
||||
"partition" => Self::Partition,
|
||||
"sticker" => Self::Sticker,
|
||||
"subscription" => Self::Subscription,
|
||||
"message" => Self::Message,
|
||||
"neighbor" => Self::Neighbor,
|
||||
"mount" => Self::Mount,
|
||||
other => Self::Other(other.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Audio {
|
||||
pub sample_rate: u64,
|
||||
pub bits: u8,
|
||||
pub channels: u8,
|
||||
}
|
||||
|
||||
impl FromStr for Audio {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut parts = s.split(':');
|
||||
let sample_rate = parts.next().ok_or(())?.parse().map_err(|_| ())?;
|
||||
let bits = u8::from_str_radix(parts.next().ok_or(())?, 16).map_err(|_| ())? + 1;
|
||||
let channels = parts.next().ok_or(())?.parse().map_err(|_| ())?;
|
||||
Ok(Self {
|
||||
sample_rate,
|
||||
bits,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum BoolOrOneshot {
|
||||
True,
|
||||
False,
|
||||
Oneshot,
|
||||
}
|
||||
|
||||
impl FromStr for BoolOrOneshot {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"0" => Self::False,
|
||||
"1" => Self::True,
|
||||
"oneshot" => Self::Oneshot,
|
||||
_ => return Err(()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
32
src/filter.rs
Normal file
32
src/filter.rs
Normal file
@ -0,0 +1,32 @@
|
||||
use crate::common::{Priority, Tag};
|
||||
|
||||
pub enum CaseSensitivity {
|
||||
CaseSensitive,
|
||||
CaseInsensitive,
|
||||
CommandDependent,
|
||||
}
|
||||
|
||||
pub enum Filter {
|
||||
Not(Box<Filter>),
|
||||
And(Box<Filter>, Box<Filter>),
|
||||
|
||||
EqTag(Tag, String, CaseSensitivity),
|
||||
Contains(Tag, String, CaseSensitivity),
|
||||
StartsWith(Tag, String, CaseSensitivity),
|
||||
PerlRegex(Tag, String),
|
||||
NegPerlRegex(Tag, String),
|
||||
EqUri(String),
|
||||
Base(String),
|
||||
ModifiedSince(u64),
|
||||
AudioFormatEq {
|
||||
sample_rate: u32,
|
||||
bits: u8,
|
||||
channels: u8,
|
||||
},
|
||||
AudioFormatEqMask {
|
||||
sample_rate: Option<u32>,
|
||||
bits: Option<u8>,
|
||||
channels: Option<u8>,
|
||||
},
|
||||
PrioCmp(Priority),
|
||||
}
|
12
src/lib.rs
12
src/lib.rs
@ -1,11 +1,15 @@
|
||||
///! This library contains structs and parsing for the mpd protocol
|
||||
///! as described in https://mpd.readthedocs.io/en/latest/protocol.html#protocol-overview
|
||||
///!
|
||||
///! It does not provide any client or server implementation
|
||||
//! This library contains structs and parsing for the mpd protocol
|
||||
//! as described in https://mpd.readthedocs.io/en/latest/protocol.html#protocol-overview
|
||||
//!
|
||||
//! It does not provide any client or server implementation
|
||||
|
||||
mod commands;
|
||||
mod common;
|
||||
mod filter;
|
||||
mod request;
|
||||
mod response;
|
||||
mod server;
|
||||
|
||||
pub use request::Request;
|
||||
pub use response::Response;
|
||||
pub use server::MPDServer;
|
||||
|
596
src/request.rs
596
src/request.rs
@ -1,302 +1,360 @@
|
||||
type SongPosition = u32;
|
||||
type SongId = u32;
|
||||
type Seconds = u32;
|
||||
type TimeWithFractions = f64;
|
||||
type OneOrRange = (SongPosition, Option<SongPosition>);
|
||||
type WindowRange = (Option<SongPosition>, Option<SongPosition>);
|
||||
type Priority = u8;
|
||||
type PlaylistName = String;
|
||||
type Offset = u32;
|
||||
use std::str::FromStr;
|
||||
|
||||
// TODO: use a proper types
|
||||
type TagName = String;
|
||||
type TagValue = String;
|
||||
type Uri = String;
|
||||
type Path = String;
|
||||
type Filter = String;
|
||||
type Sort = String;
|
||||
type Version = String;
|
||||
type Tag = String;
|
||||
type Feature = String;
|
||||
type PartitionName = String;
|
||||
type AudioOutputId = String;
|
||||
type ChannelName = String;
|
||||
type StickerType = String;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
use crate::common::*;
|
||||
|
||||
use crate::commands::*;
|
||||
|
||||
// TODO: SingleLineString
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Request {
|
||||
// -- Query Commands -- //
|
||||
ClearError,
|
||||
CurrentSong,
|
||||
Idle(Option<Vec<SubSystem>>),
|
||||
Status,
|
||||
Stats,
|
||||
CommandList(Vec<Request>),
|
||||
|
||||
// -- Playback Commands -- //
|
||||
Consume(ConsumeState),
|
||||
Crossfade(Seconds),
|
||||
MixRampDb(f32),
|
||||
MixRampDelay(Seconds),
|
||||
Random(bool),
|
||||
Repeat(bool),
|
||||
SetVol(Volume),
|
||||
GetVol,
|
||||
Single(SingleState),
|
||||
ReplayGainMode(ReplayGainMode),
|
||||
ReplayGainStatus,
|
||||
Volume(Volume),
|
||||
// -- Query Commands -- //
|
||||
ClearError,
|
||||
CurrentSong,
|
||||
Idle(Option<Vec<SubSystem>>),
|
||||
Status,
|
||||
Stats,
|
||||
|
||||
// -- Playback Control Commands -- //
|
||||
Next,
|
||||
Pause(Option<bool>),
|
||||
Play(SongPosition),
|
||||
PlayId(SongId),
|
||||
Previous,
|
||||
Seek(SongPosition, TimeWithFractions),
|
||||
SeekId(SongId, TimeWithFractions),
|
||||
SeekCur(SeekMode, TimeWithFractions),
|
||||
Stop,
|
||||
// -- Playback Commands -- //
|
||||
Consume(BoolOrOneshot),
|
||||
Crossfade(Seconds),
|
||||
MixRampDb(f32),
|
||||
MixRampDelay(Seconds),
|
||||
Random(bool),
|
||||
Repeat(bool),
|
||||
SetVol(Volume),
|
||||
GetVol,
|
||||
Single(BoolOrOneshot),
|
||||
ReplayGainMode(ReplayGainModeMode),
|
||||
ReplayGainStatus,
|
||||
Volume(Volume),
|
||||
|
||||
// -- Queue Commands -- //
|
||||
// TODO: relative mode
|
||||
Add(String, Option<SongPosition>),
|
||||
// TODO: relative mode
|
||||
AddId(String, Option<SongPosition>),
|
||||
Clear,
|
||||
Delete(OneOrRange),
|
||||
DeleteId(SongId),
|
||||
Move(OneOrRange, SongPosition),
|
||||
MoveId(SongId, SongPosition),
|
||||
Playlist,
|
||||
PlaylistFind(Filter, Option<Sort>, Option<WindowRange>),
|
||||
PlaylistId(SongId),
|
||||
PlaylistInfo(OneOrRange),
|
||||
PlaylistSearch(Filter, Option<Sort>, Option<WindowRange>),
|
||||
// TODO: which type of range?
|
||||
PlChanges(Version, Option<WindowRange>),
|
||||
// TODO: which type of range?
|
||||
PlChangesPosId(Version, Option<WindowRange>),
|
||||
// TODO: which type of range?
|
||||
Prio(Priority, WindowRange),
|
||||
PrioId(Priority, Vec<SongId>),
|
||||
RangeId(SongId, WindowRange),
|
||||
Shuffle(Option<OneOrRange>),
|
||||
Swap(SongPosition, SongPosition),
|
||||
SwapId(SongId, SongId),
|
||||
AddTagId(SongId, TagName, TagValue),
|
||||
ClearTagId(SongId, TagName),
|
||||
// -- Playback Control Commands -- //
|
||||
Next,
|
||||
Pause(Option<bool>),
|
||||
Play(SongPosition),
|
||||
PlayId(SongId),
|
||||
Previous,
|
||||
Seek(SongPosition, TimeWithFractions),
|
||||
SeekId(SongId, TimeWithFractions),
|
||||
SeekCur(SeekMode, TimeWithFractions),
|
||||
Stop,
|
||||
|
||||
// -- Stored Playlist Commands -- //
|
||||
ListPlaylist(PlaylistName, Option<WindowRange>),
|
||||
ListPlaylistInfo(PlaylistName, Option<WindowRange>),
|
||||
SearchPlaylist(PlaylistName, Filter, Option<WindowRange>),
|
||||
ListPlaylists,
|
||||
Load(PlaylistName, Option<WindowRange>, SongPosition),
|
||||
PlaylistAdd(PlaylistName, Uri, SongPosition),
|
||||
PlaylistClear(PlaylistName),
|
||||
PlaylistDelete(PlaylistName, OneOrRange),
|
||||
PlaylistLength(PlaylistName),
|
||||
// TODO: which type of range?
|
||||
PlaylistMove(PlaylistName, OneOrRange, SongPosition),
|
||||
Rename(PlaylistName, PlaylistName),
|
||||
Rm(PlaylistName),
|
||||
Save(PlaylistName, Option<SaveMode>),
|
||||
// -- Queue Commands -- //
|
||||
// TODO: relative mode
|
||||
Add(String, Option<SongPosition>),
|
||||
// TODO: relative mode
|
||||
AddId(String, Option<SongPosition>),
|
||||
Clear,
|
||||
Delete(OneOrRange),
|
||||
DeleteId(SongId),
|
||||
Move(OneOrRange, SongPosition),
|
||||
MoveId(SongId, SongPosition),
|
||||
Playlist,
|
||||
PlaylistFind(Filter, Option<Sort>, Option<WindowRange>),
|
||||
PlaylistId(SongId),
|
||||
PlaylistInfo(OneOrRange),
|
||||
PlaylistSearch(Filter, Option<Sort>, Option<WindowRange>),
|
||||
// TODO: which type of range?
|
||||
PlChanges(Version, Option<WindowRange>),
|
||||
// TODO: which type of range?
|
||||
PlChangesPosId(Version, Option<WindowRange>),
|
||||
// TODO: which type of range?
|
||||
Prio(Priority, WindowRange),
|
||||
PrioId(Priority, Vec<SongId>),
|
||||
RangeId(SongId, WindowRange),
|
||||
Shuffle(Option<OneOrRange>),
|
||||
Swap(SongPosition, SongPosition),
|
||||
SwapId(SongId, SongId),
|
||||
AddTagId(SongId, TagName, TagValue),
|
||||
ClearTagId(SongId, TagName),
|
||||
|
||||
// -- Music Database Commands -- //
|
||||
AlbumArt(Uri, Offset),
|
||||
Count(Filter, Option<GroupType>),
|
||||
GetFingerprint(Uri),
|
||||
Find(Filter, Option<Sort>, Option<WindowRange>),
|
||||
FindAdd(Filter, Option<Sort>, Option<WindowRange>, Option<SongPosition>),
|
||||
List(Tag, Filter, Option<GroupType>),
|
||||
#[deprecated]
|
||||
ListAll(Option<Uri>),
|
||||
#[deprecated]
|
||||
ListAllInfo(Option<Uri>),
|
||||
ListFiles(Uri),
|
||||
LsInfo(Option<Uri>),
|
||||
ReadComments(Uri),
|
||||
ReadPicture(Uri, Offset),
|
||||
Search(Filter, Option<Sort>, Option<WindowRange>),
|
||||
SearchAdd(Filter, Option<Sort>, Option<WindowRange>, Option<SongPosition>),
|
||||
SearchAddPl(Filter, Option<Sort>, Option<WindowRange>, Option<SongPosition>),
|
||||
SearchCount(Filter, Option<GroupType>),
|
||||
Update(Option<Uri>),
|
||||
Rescan(Option<Uri>),
|
||||
// -- Stored Playlist Commands -- //
|
||||
ListPlaylist(PlaylistName, Option<WindowRange>),
|
||||
ListPlaylistInfo(PlaylistName, Option<WindowRange>),
|
||||
SearchPlaylist(PlaylistName, Filter, Option<WindowRange>),
|
||||
ListPlaylists,
|
||||
Load(PlaylistName, Option<WindowRange>, SongPosition),
|
||||
PlaylistAdd(PlaylistName, Uri, SongPosition),
|
||||
PlaylistClear(PlaylistName),
|
||||
PlaylistDelete(PlaylistName, OneOrRange),
|
||||
PlaylistLength(PlaylistName),
|
||||
// TODO: which type of range?
|
||||
PlaylistMove(PlaylistName, OneOrRange, SongPosition),
|
||||
Rename(PlaylistName, PlaylistName),
|
||||
Rm(PlaylistName),
|
||||
Save(PlaylistName, Option<SaveMode>),
|
||||
|
||||
// -- Mount and Neighbor Commands -- //
|
||||
Mount(Option<Path>, Option<Uri>),
|
||||
Unmount(Path),
|
||||
ListMounts,
|
||||
ListNeighbors,
|
||||
// -- Music Database Commands -- //
|
||||
AlbumArt(Uri, Offset),
|
||||
Count(Filter, Option<GroupType>),
|
||||
GetFingerprint(Uri),
|
||||
Find(Filter, Option<Sort>, Option<WindowRange>),
|
||||
FindAdd(
|
||||
Filter,
|
||||
Option<Sort>,
|
||||
Option<WindowRange>,
|
||||
Option<SongPosition>,
|
||||
),
|
||||
List(Tag, Filter, Option<GroupType>),
|
||||
#[deprecated]
|
||||
ListAll(Option<Uri>),
|
||||
#[deprecated]
|
||||
ListAllInfo(Option<Uri>),
|
||||
ListFiles(Uri),
|
||||
LsInfo(Option<Uri>),
|
||||
ReadComments(Uri),
|
||||
ReadPicture(Uri, Offset),
|
||||
Search(Filter, Option<Sort>, Option<WindowRange>),
|
||||
SearchAdd(
|
||||
Filter,
|
||||
Option<Sort>,
|
||||
Option<WindowRange>,
|
||||
Option<SongPosition>,
|
||||
),
|
||||
SearchAddPl(
|
||||
Filter,
|
||||
Option<Sort>,
|
||||
Option<WindowRange>,
|
||||
Option<SongPosition>,
|
||||
),
|
||||
SearchCount(Filter, Option<GroupType>),
|
||||
Update(Option<Uri>),
|
||||
Rescan(Option<Uri>),
|
||||
|
||||
// -- Sticker Commands -- //
|
||||
StickerGet(StickerType, Uri, String),
|
||||
StickerSet(StickerType, Uri, String, String),
|
||||
StickerDelete(StickerType, Uri, String),
|
||||
StickerList(StickerType, Uri),
|
||||
StickerFind(StickerType, Uri, String, Option<Sort>, Option<WindowRange>),
|
||||
StickerFindValue(StickerType, Uri, String, String, Option<Sort>, Option<WindowRange>),
|
||||
StickerNames,
|
||||
StickerTypes,
|
||||
StickerNamesTypes(Option<StickerType>),
|
||||
// -- Mount and Neighbor Commands -- //
|
||||
Mount(Option<Path>, Option<Uri>),
|
||||
Unmount(Path),
|
||||
ListMounts,
|
||||
ListNeighbors,
|
||||
|
||||
// -- Connection Commands -- //
|
||||
Close,
|
||||
Kill,
|
||||
Password(String),
|
||||
Ping,
|
||||
BinaryLimit(u64),
|
||||
TagTypes,
|
||||
TagTypesDisable(Vec<Tag>),
|
||||
TagTypesEnable(Vec<Tag>),
|
||||
TagTypesClear,
|
||||
TagTypesAll,
|
||||
TagTypesAvailable,
|
||||
TagTypesReset(Vec<Tag>),
|
||||
Protocol,
|
||||
ProtocolDisable(Vec<Feature>),
|
||||
ProtocolEnable(Vec<Feature>),
|
||||
ProtocolClear,
|
||||
ProtocolAll,
|
||||
ProtocolAvailable,
|
||||
// -- Sticker Commands -- //
|
||||
StickerGet(StickerType, Uri, String),
|
||||
StickerSet(StickerType, Uri, String, String),
|
||||
StickerDelete(StickerType, Uri, String),
|
||||
StickerList(StickerType, Uri),
|
||||
StickerFind(StickerType, Uri, String, Option<Sort>, Option<WindowRange>),
|
||||
StickerFindValue(
|
||||
StickerType,
|
||||
Uri,
|
||||
String,
|
||||
String,
|
||||
Option<Sort>,
|
||||
Option<WindowRange>,
|
||||
),
|
||||
StickerNames,
|
||||
StickerTypes,
|
||||
StickerNamesTypes(Option<StickerType>),
|
||||
|
||||
// -- Partition Commands -- //
|
||||
Partition(PartitionName),
|
||||
ListPartitions,
|
||||
NewPartition(PartitionName),
|
||||
DelPartition(PartitionName),
|
||||
MoveOutput(String),
|
||||
// -- Connection Commands -- //
|
||||
Close,
|
||||
Kill,
|
||||
Password(String),
|
||||
Ping,
|
||||
BinaryLimit(u64),
|
||||
TagTypes,
|
||||
TagTypesDisable(Vec<Tag>),
|
||||
TagTypesEnable(Vec<Tag>),
|
||||
TagTypesClear,
|
||||
TagTypesAll,
|
||||
TagTypesAvailable,
|
||||
TagTypesReset(Vec<Tag>),
|
||||
Protocol,
|
||||
ProtocolDisable(Vec<Feature>),
|
||||
ProtocolEnable(Vec<Feature>),
|
||||
ProtocolClear,
|
||||
ProtocolAll,
|
||||
ProtocolAvailable,
|
||||
|
||||
// -- Audio Output Commands -- //
|
||||
DisableOutput(AudioOutputId),
|
||||
EnableOutput(AudioOutputId),
|
||||
ToggleOutput(AudioOutputId),
|
||||
Outputs,
|
||||
OutputSet(AudioOutputId, String, String),
|
||||
// -- Partition Commands -- //
|
||||
Partition(PartitionName),
|
||||
ListPartitions,
|
||||
NewPartition(PartitionName),
|
||||
DelPartition(PartitionName),
|
||||
MoveOutput(String),
|
||||
|
||||
// -- Reflection Commands -- //
|
||||
Config,
|
||||
Commands,
|
||||
NotCommands,
|
||||
UrlHandlers,
|
||||
Decoders,
|
||||
// -- Audio Output Commands -- //
|
||||
DisableOutput(AudioOutputId),
|
||||
EnableOutput(AudioOutputId),
|
||||
ToggleOutput(AudioOutputId),
|
||||
Outputs,
|
||||
OutputSet(AudioOutputId, String, String),
|
||||
|
||||
// -- Client to Client Commands -- //
|
||||
Subscribe(ChannelName),
|
||||
Unsubscribe(ChannelName),
|
||||
Channels,
|
||||
ReadMessages,
|
||||
SendMessage(ChannelName, String),
|
||||
// -- Reflection Commands -- //
|
||||
Config,
|
||||
Commands,
|
||||
NotCommands,
|
||||
UrlHandlers,
|
||||
Decoders,
|
||||
|
||||
// -- Client to Client Commands -- //
|
||||
Subscribe(ChannelName),
|
||||
Unsubscribe(ChannelName),
|
||||
Channels,
|
||||
ReadMessages,
|
||||
SendMessage(ChannelName, String),
|
||||
}
|
||||
|
||||
impl Into<Vec<u8>> for Request {
|
||||
fn into(self) -> Vec<u8> {
|
||||
todo!()
|
||||
// impl From<Request> for Vec<u8> {
|
||||
// fn from(val: Request) -> Self {
|
||||
// todo!()
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SaveMode {
|
||||
Create,
|
||||
Append,
|
||||
Replace,
|
||||
}
|
||||
impl FromStr for SaveMode {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"create" => Ok(Self::Create),
|
||||
"append" => Ok(Self::Append),
|
||||
"replace" => Ok(Self::Replace),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SaveMode {
|
||||
Create,
|
||||
Append,
|
||||
Replace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SeekMode {
|
||||
Relative,
|
||||
RelativeReverse,
|
||||
Absolute,
|
||||
Relative,
|
||||
RelativeReverse,
|
||||
Absolute,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SubSystem {
|
||||
/// The song database has been modified after update.
|
||||
Database,
|
||||
/// A database update has started or finished. If the database was modified during the update, the database event is also emitted.
|
||||
Update,
|
||||
/// A stored playlist has been modified, renamed, created or deleted
|
||||
StoredPlaylist,
|
||||
/// The queue (i.e. the current playlist) has been modified
|
||||
Playlist,
|
||||
/// The player has been started, stopped or seeked or tags of the currently playing song have changed (e.g. received from stream)
|
||||
Player,
|
||||
/// The volume has been changed
|
||||
Mixer,
|
||||
/// An audio output has been added, removed or modified (e.g. renamed, enabled or disabled)
|
||||
Output,
|
||||
/// Options like repeat, random, crossfade, replay gain
|
||||
Options,
|
||||
/// A partition was added, removed or changed
|
||||
Partition,
|
||||
/// The sticker database has been modified.
|
||||
Sticker,
|
||||
/// A client has subscribed or unsubscribed to a channel
|
||||
Subscription,
|
||||
/// A message was received on a channel this client is subscribed to; this event is only emitted when the client’s message queue is empty
|
||||
Message,
|
||||
/// A neighbor was found or lost
|
||||
Neighbor,
|
||||
/// The mount list has changed
|
||||
Mount,
|
||||
|
||||
/// Other subsystems not covered by the above
|
||||
Other(String),
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ReplayGainModeMode {
|
||||
Off,
|
||||
Track,
|
||||
Album,
|
||||
Auto,
|
||||
}
|
||||
impl FromStr for ReplayGainModeMode {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"off" => Ok(Self::Off),
|
||||
"track" => Ok(Self::Track),
|
||||
"album" => Ok(Self::Album),
|
||||
"auto" => Ok(Self::Auto),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ConsumeState {
|
||||
On,
|
||||
Off,
|
||||
Oneshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SingleState {
|
||||
On,
|
||||
Off,
|
||||
Oneshot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ReplayGainMode {
|
||||
Off,
|
||||
Track,
|
||||
Album,
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Volume(u32);
|
||||
impl Volume {
|
||||
pub fn new(volume: u32) -> Result<Self, ()> {
|
||||
match volume {
|
||||
0..=100 => Ok(Self(volume)),
|
||||
_ => Err(()),
|
||||
pub fn new(volume: u32) -> Result<Self, ()> {
|
||||
match volume {
|
||||
0..=100 => Ok(Self(volume)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Into<u32> for Volume {
|
||||
fn into(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
impl From<Volume> for u32 {
|
||||
fn from(val: Volume) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
impl FromStr for Volume {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let volume = s.parse().map_err(|_| ())?;
|
||||
Volume::new(volume)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: fill out
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum GroupType {
|
||||
Artist,
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Date,
|
||||
Genre,
|
||||
Track,
|
||||
Composer,
|
||||
Performer,
|
||||
Conductor,
|
||||
Comment,
|
||||
Disc,
|
||||
Filename,
|
||||
Any,
|
||||
}
|
||||
Artist,
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Date,
|
||||
Genre,
|
||||
Track,
|
||||
Composer,
|
||||
Performer,
|
||||
Conductor,
|
||||
Comment,
|
||||
Disc,
|
||||
Filename,
|
||||
Any,
|
||||
}
|
||||
|
||||
// trait RequestCommand {
|
||||
// // 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(parts: SplitWhitespace<'_>) -> RequestParserResponse<'_>;
|
||||
// }
|
||||
|
||||
// type RequestParserResponse<'a> = Result<(Request, &'a str), RequestParserError>;
|
||||
|
||||
// pub enum RequestParserError {
|
||||
// SyntaxError(u64, String),
|
||||
// MissingCommandListEnd(u64),
|
||||
// NestedCommandList(u64),
|
||||
// UnexpectedCommandListEnd(u64),
|
||||
// UnexpectedEOF,
|
||||
// MissingNewline,
|
||||
// }
|
||||
|
||||
// TODO: upon encountering an error, there should be a function that lets you skip to the next OK,
|
||||
// and continue execution. Maybe "parse_next_or_skip(&str) -> RequestParserResponse", which
|
||||
// could skip stuff internally? Or do we want to report back the error with the entire command
|
||||
// and let the library user decide what to do?
|
||||
|
||||
impl Request {
|
||||
pub fn parse_next(raw: &str) -> RequestParserResult<'_> {
|
||||
let (line, rest) = raw
|
||||
.split_once('\n')
|
||||
.ok_or(RequestParserError::UnexpectedEOF)?;
|
||||
let mut parts = line.split_whitespace();
|
||||
|
||||
match parts
|
||||
.next()
|
||||
.ok_or(RequestParserError::SyntaxError(0, line.to_string()))?
|
||||
.trim()
|
||||
{
|
||||
"command_list_begin" => {
|
||||
let mut commands = Vec::new();
|
||||
let mut i = 1;
|
||||
loop {
|
||||
i += 1;
|
||||
let (line, rest) = rest
|
||||
.split_once('\n')
|
||||
.ok_or(RequestParserError::MissingCommandListEnd(i))?;
|
||||
match line.trim() {
|
||||
"command_list_begin" => {
|
||||
return Err(RequestParserError::NestedCommandList(i))
|
||||
}
|
||||
"command_list_end" => {
|
||||
return Ok((Request::CommandList(commands), rest));
|
||||
}
|
||||
input => {
|
||||
let (command, _) = Request::parse_next(input)?;
|
||||
commands.push(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"command_list_end" => Err(RequestParserError::UnexpectedCommandListEnd(0)),
|
||||
|
||||
Idle::COMMAND => Idle::parse_request(parts).map(|(req, _)| (req, rest)),
|
||||
Status::COMMAND => Status::parse_request(parts).map(|(req, _)| (req, rest)),
|
||||
ClearError::COMMAND => ClearError::parse_request(parts).map(|(req, _)| (req, rest)),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,37 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// See https://github.com/MusicPlayerDaemon/MPD/blob/7774c3369e1484dc5dec6d7d9572e0a57e9c5302/src/command/AllCommands.cxx#L67-L209
|
||||
pub enum Response {
|
||||
Ok,
|
||||
GenericError(String),
|
||||
Ok,
|
||||
GenericError(String),
|
||||
}
|
||||
|
||||
impl From<()> for Response {
|
||||
fn from(_: ()) -> Self {
|
||||
Response::Ok
|
||||
}
|
||||
}
|
||||
|
||||
// impl From<Error> for Response {
|
||||
// fn from(e: Error) -> Self {
|
||||
// Response::GenericError(e.to_string())
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GetFingerprintResponse {
|
||||
pub chromaprint: String,
|
||||
}
|
||||
|
||||
pub struct Neighbor {
|
||||
pub neighbor: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct NeighborResponse {
|
||||
pub neighbors: Vec<Neighbor>,
|
||||
}
|
||||
|
||||
pub struct UrlHandlersResponse {
|
||||
pub url_handlers: Vec<String>,
|
||||
}
|
||||
|
246
src/server.rs
Normal file
246
src/server.rs
Normal file
@ -0,0 +1,246 @@
|
||||
use crate::{common::SubSystem, Request, Response};
|
||||
|
||||
pub trait MPDServer {
|
||||
type Error;
|
||||
|
||||
fn route_request(&mut self, request: Request) -> Result<Response, Self::Error> {
|
||||
match request {
|
||||
Request::ClearError => self.handle_clear_error().map(|_| Response::Ok),
|
||||
Request::CurrentSong => self.handle_current_song().map(|_| Response::Ok),
|
||||
Request::Idle(subsystems) => self.handle_idle(subsystems).map(|_| Response::Ok),
|
||||
Request::Status => self.handle_status().map(|_| Response::Ok),
|
||||
Request::Stats => self.handle_stats().map(|_| Response::Ok),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_unimplemented(&mut self) -> Result<(), Self::Error> {
|
||||
// fn handle_unimplemented(&mut self, name: &str) -> Result<(), Self::Error> {
|
||||
// return Err("a".into());
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// -- Query Commands -- //
|
||||
fn handle_clear_error(&mut self) -> Result<(), Self::Error> {
|
||||
self.handle_unimplemented()
|
||||
}
|
||||
fn handle_current_song(&mut self) -> Result<(), Self::Error> {
|
||||
self.handle_unimplemented()
|
||||
}
|
||||
fn handle_idle(&mut self, _: Option<Vec<SubSystem>>) -> Result<(), Self::Error> {
|
||||
self.handle_unimplemented()
|
||||
}
|
||||
fn handle_status(&mut self) -> Result<(), Self::Error> {
|
||||
self.handle_unimplemented()
|
||||
}
|
||||
fn handle_stats(&mut self) -> Result<(), Self::Error> {
|
||||
self.handle_unimplemented()
|
||||
}
|
||||
|
||||
// -- Playback Commands -- //
|
||||
// handle_consume(ConsumeState),
|
||||
// handle_crossfade(Seconds),
|
||||
// handle_mix_ramp_db(f32),
|
||||
// handle_mix_ramp_delay(Seconds),
|
||||
// handle_random(bool),
|
||||
// handle_repeat(bool),
|
||||
// handle_set_vol(Volume),
|
||||
// handle_get_vol,
|
||||
// handle_single(SingleState),
|
||||
// handle_replay_gain_mode(ReplayGainMode),
|
||||
// handle_replay_gain_status,
|
||||
// handle_volume(Volume),
|
||||
|
||||
// // -- Playback Control Commands -- //
|
||||
// next,
|
||||
// pause(Option<bool>),
|
||||
// play(SongPosition),
|
||||
// play_id(SongId),
|
||||
// previous,
|
||||
// seek(SongPosition, TimeWithFractions),
|
||||
// seek_id(SongId, TimeWithFractions),
|
||||
// seek_cur(SeekMode, TimeWithFractions),
|
||||
// stop,
|
||||
|
||||
// // -- Queue Commands -- //
|
||||
// handle_add(String, Option<SongPosition>),
|
||||
// handle_add_id(String, Option<SongPosition>),
|
||||
// handle_clear,
|
||||
// handle_delete(OneOrRange),
|
||||
// handle_delete_id(SongId),
|
||||
// handle_move(OneOrRange, SongPosition),
|
||||
// handle_move_id(SongId, SongPosition),
|
||||
// handle_playlist,
|
||||
// handle_playlist_find(Filter, Option<Sort>, Option<WindowRange>),
|
||||
// handle_playlist_id(SongId),
|
||||
// handle_playlist_info(OneOrRange),
|
||||
// handle_playlist_search(Filter, Option<Sort>, Option<WindowRange>),
|
||||
// handle_pl_changes(Version, Option<WindowRange>),
|
||||
// handle_pl_changes_pos_id(Version, Option<WindowRange>),
|
||||
// handle_prio(Priority, WindowRange),
|
||||
// handle_prio_id(Priority, Vec<SongId>),
|
||||
// handle_range_id(SongId, WindowRange),
|
||||
// handle_shuffle(Option<OneOrRange>),
|
||||
// handle_swap(SongPosition, SongPosition),
|
||||
// handle_swap_id(SongId, SongId),
|
||||
// handle_add_tag_id(SongId, TagName, TagValue),
|
||||
// handle_clear_tag_id(SongId, TagName),
|
||||
|
||||
// // -- Stored Playlist Commands -- //
|
||||
// ListPlaylist(PlaylistName, Option<WindowRange>),
|
||||
// ListPlaylistInfo(PlaylistName, Option<WindowRange>),
|
||||
// SearchPlaylist(PlaylistName, Filter, Option<WindowRange>),
|
||||
// ListPlaylists,
|
||||
// Load(PlaylistName, Option<WindowRange>, SongPosition),
|
||||
// PlaylistAdd(PlaylistName, Uri, SongPosition),
|
||||
// PlaylistClear(PlaylistName),
|
||||
// PlaylistDelete(PlaylistName, OneOrRange),
|
||||
// PlaylistLength(PlaylistName),
|
||||
// // TODO: which type of range?
|
||||
// PlaylistMove(PlaylistName, OneOrRange, SongPosition),
|
||||
// Rename(PlaylistName, PlaylistName),
|
||||
// Rm(PlaylistName),
|
||||
// Save(PlaylistName, Option<SaveMode>),
|
||||
|
||||
// // -- Music Database Commands -- //
|
||||
// AlbumArt(Uri, Offset),
|
||||
// Count(Filter, Option<GroupType>),
|
||||
// GetFingerprint(Uri),
|
||||
// Find(Filter, Option<Sort>, Option<WindowRange>),
|
||||
// FindAdd(Filter, Option<Sort>, Option<WindowRange>, Option<SongPosition>),
|
||||
// List(Tag, Filter, Option<GroupType>),
|
||||
// #[deprecated]
|
||||
// ListAll(Option<Uri>),
|
||||
// #[deprecated]
|
||||
// ListAllInfo(Option<Uri>),
|
||||
// ListFiles(Uri),
|
||||
// LsInfo(Option<Uri>),
|
||||
// ReadComments(Uri),
|
||||
// ReadPicture(Uri, Offset),
|
||||
// Search(Filter, Option<Sort>, Option<WindowRange>),
|
||||
// SearchAdd(Filter, Option<Sort>, Option<WindowRange>, Option<SongPosition>),
|
||||
// SearchAddPl(Filter, Option<Sort>, Option<WindowRange>, Option<SongPosition>),
|
||||
// SearchCount(Filter, Option<GroupType>),
|
||||
// Update(Option<Uri>),
|
||||
// Rescan(Option<Uri>),
|
||||
|
||||
// // -- Mount and Neighbor Commands -- //
|
||||
// Mount(Option<Path>, Option<Uri>),
|
||||
// Unmount(Path),
|
||||
// ListMounts,
|
||||
// ListNeighbors,
|
||||
|
||||
// // -- Sticker Commands -- //
|
||||
// StickerGet(StickerType, Uri, String),
|
||||
// StickerSet(StickerType, Uri, String, String),
|
||||
// StickerDelete(StickerType, Uri, String),
|
||||
// StickerList(StickerType, Uri),
|
||||
// StickerFind(StickerType, Uri, String, Option<Sort>, Option<WindowRange>),
|
||||
// StickerFindValue(StickerType, Uri, String, String, Option<Sort>, Option<WindowRange>),
|
||||
// StickerNames,
|
||||
// StickerTypes,
|
||||
// StickerNamesTypes(Option<StickerType>),
|
||||
|
||||
// // -- Connection Commands -- //
|
||||
// Close,
|
||||
// Kill,
|
||||
// Password(String),
|
||||
// Ping,
|
||||
// BinaryLimit(u64),
|
||||
// TagTypes,
|
||||
// TagTypesDisable(Vec<Tag>),
|
||||
// TagTypesEnable(Vec<Tag>),
|
||||
// TagTypesClear,
|
||||
// TagTypesAll,
|
||||
// TagTypesAvailable,
|
||||
// TagTypesReset(Vec<Tag>),
|
||||
// Protocol,
|
||||
// ProtocolDisable(Vec<Feature>),
|
||||
// ProtocolEnable(Vec<Feature>),
|
||||
// ProtocolClear,
|
||||
// ProtocolAll,
|
||||
// ProtocolAvailable,
|
||||
|
||||
// // -- Partition Commands -- //
|
||||
// Partition(PartitionName),
|
||||
// ListPartitions,
|
||||
// NewPartition(PartitionName),
|
||||
// DelPartition(PartitionName),
|
||||
// MoveOutput(String),
|
||||
|
||||
// // -- Audio Output Commands -- //
|
||||
// DisableOutput(AudioOutputId),
|
||||
// EnableOutput(AudioOutputId),
|
||||
// ToggleOutput(AudioOutputId),
|
||||
// Outputs,
|
||||
// OutputSet(AudioOutputId, String, String),
|
||||
|
||||
// // -- Reflection Commands -- //
|
||||
// Config,
|
||||
// Commands,
|
||||
// NotCommands,
|
||||
// UrlHandlers,
|
||||
// Decoders,
|
||||
|
||||
// // -- Client to Client Commands -- //
|
||||
// Subscribe(ChannelName),
|
||||
// Unsubscribe(ChannelName),
|
||||
// Channels,
|
||||
// ReadMessages,
|
||||
// SendMessage(ChannelName, String),
|
||||
}
|
||||
|
||||
// impl Into<Vec<u8>> for Request {
|
||||
// fn into(self) -> Vec<u8> {
|
||||
// todo!()
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
// pub enum SaveMode {
|
||||
// Create,
|
||||
// Append,
|
||||
// Replace,
|
||||
// }
|
||||
|
||||
// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
// pub enum SeekMode {
|
||||
// Relative,
|
||||
// RelativeReverse,
|
||||
// Absolute,
|
||||
// }
|
||||
|
||||
// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
// pub enum SubSystem {
|
||||
// /// The song database has been modified after update.
|
||||
// Database,
|
||||
// /// A database update has started or finished. If the database was modified during the update, the database event is also emitted.
|
||||
// Update,
|
||||
// /// A stored playlist has been modified, renamed, created or deleted
|
||||
// StoredPlaylist,
|
||||
// /// The queue (i.e. the current playlist) has been modified
|
||||
// Playlist,
|
||||
// /// The player has been started, stopped or seeked or tags of the currently playing song have changed (e.g. received from stream)
|
||||
// Player,
|
||||
// /// The volume has been changed
|
||||
// Mixer,
|
||||
// /// An audio output has been added, removed or modified (e.g. renamed, enabled or disabled)
|
||||
// Output,
|
||||
// /// Options like repeat, random, crossfade, replay gain
|
||||
// Options,
|
||||
// /// A partition was added, removed or changed
|
||||
// Partition,
|
||||
// /// The sticker database has been modified.
|
||||
// Sticker,
|
||||
// /// A client has subscribed or unsubscribed to a channel
|
||||
// Subscription,
|
||||
// /// A message was received on a channel this client is subscribed to; this event is only emitted when the client’s message queue is empty
|
||||
// Message,
|
||||
// /// A neighbor was found or lost
|
||||
// Neighbor,
|
||||
// /// The mount list has changed
|
||||
// Mount,
|
||||
|
||||
// /// Other subsystems not covered by the above
|
||||
// Other(String),
|
||||
// }
|
Loading…
Reference in New Issue
Block a user