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.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "empidee"
|
name = "empidee"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"indoc",
|
||||||
|
"pretty_assertions",
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.88"
|
version = "1.0.88"
|
||||||
@ -63,3 +87,9 @@ name = "unicode-ident"
|
|||||||
version = "1.0.13"
|
version = "1.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
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"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[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)]
|
use serde::{Deserialize, Serialize};
|
||||||
pub(crate) enum Tag {
|
|
||||||
|
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.
|
/// The artist name. Its meaning is not well-defined; see “composer” and “performer” for more specific tags.
|
||||||
Artist(String),
|
Artist(String),
|
||||||
/// Same as artist, but for sorting. This usually omits prefixes such as “The”.
|
/// Same as artist, but for sorting. This usually omits prefixes such as “The”.
|
||||||
@ -25,9 +52,9 @@ pub(crate) enum Tag {
|
|||||||
Genre(String),
|
Genre(String),
|
||||||
/// The mood of the audio with a few keywords.
|
/// The mood of the audio with a few keywords.
|
||||||
Mood(String),
|
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),
|
Date(String),
|
||||||
/// The song’s original release date.
|
/// The song's original release date.
|
||||||
OriginalDate(String),
|
OriginalDate(String),
|
||||||
/// The artist who composed the song.
|
/// The artist who composed the song.
|
||||||
Composer(String),
|
Composer(String),
|
||||||
@ -75,3 +102,108 @@ pub(crate) enum Tag {
|
|||||||
/// Other tags not covered by the above
|
/// Other tags not covered by the above
|
||||||
Other(String, String),
|
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
|
//! This library contains structs and parsing for the mpd protocol
|
||||||
///! as described in https://mpd.readthedocs.io/en/latest/protocol.html#protocol-overview
|
//! as described in https://mpd.readthedocs.io/en/latest/protocol.html#protocol-overview
|
||||||
///!
|
//!
|
||||||
///! It does not provide any client or server implementation
|
//! It does not provide any client or server implementation
|
||||||
|
|
||||||
|
mod commands;
|
||||||
mod common;
|
mod common;
|
||||||
|
mod filter;
|
||||||
mod request;
|
mod request;
|
||||||
mod response;
|
mod response;
|
||||||
|
mod server;
|
||||||
|
|
||||||
pub use request::Request;
|
pub use request::Request;
|
||||||
pub use response::Response;
|
pub use response::Response;
|
||||||
|
pub use server::MPDServer;
|
||||||
|
596
src/request.rs
596
src/request.rs
@ -1,302 +1,360 @@
|
|||||||
type SongPosition = u32;
|
use std::str::FromStr;
|
||||||
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;
|
|
||||||
|
|
||||||
// TODO: use a proper types
|
use serde::{Deserialize, Serialize};
|
||||||
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;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
use crate::common::*;
|
||||||
|
|
||||||
|
use crate::commands::*;
|
||||||
|
|
||||||
|
// TODO: SingleLineString
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Request {
|
pub enum Request {
|
||||||
// -- Query Commands -- //
|
CommandList(Vec<Request>),
|
||||||
ClearError,
|
|
||||||
CurrentSong,
|
|
||||||
Idle(Option<Vec<SubSystem>>),
|
|
||||||
Status,
|
|
||||||
Stats,
|
|
||||||
|
|
||||||
// -- Playback Commands -- //
|
// -- Query Commands -- //
|
||||||
Consume(ConsumeState),
|
ClearError,
|
||||||
Crossfade(Seconds),
|
CurrentSong,
|
||||||
MixRampDb(f32),
|
Idle(Option<Vec<SubSystem>>),
|
||||||
MixRampDelay(Seconds),
|
Status,
|
||||||
Random(bool),
|
Stats,
|
||||||
Repeat(bool),
|
|
||||||
SetVol(Volume),
|
|
||||||
GetVol,
|
|
||||||
Single(SingleState),
|
|
||||||
ReplayGainMode(ReplayGainMode),
|
|
||||||
ReplayGainStatus,
|
|
||||||
Volume(Volume),
|
|
||||||
|
|
||||||
// -- Playback Control Commands -- //
|
// -- Playback Commands -- //
|
||||||
Next,
|
Consume(BoolOrOneshot),
|
||||||
Pause(Option<bool>),
|
Crossfade(Seconds),
|
||||||
Play(SongPosition),
|
MixRampDb(f32),
|
||||||
PlayId(SongId),
|
MixRampDelay(Seconds),
|
||||||
Previous,
|
Random(bool),
|
||||||
Seek(SongPosition, TimeWithFractions),
|
Repeat(bool),
|
||||||
SeekId(SongId, TimeWithFractions),
|
SetVol(Volume),
|
||||||
SeekCur(SeekMode, TimeWithFractions),
|
GetVol,
|
||||||
Stop,
|
Single(BoolOrOneshot),
|
||||||
|
ReplayGainMode(ReplayGainModeMode),
|
||||||
|
ReplayGainStatus,
|
||||||
|
Volume(Volume),
|
||||||
|
|
||||||
// -- Queue Commands -- //
|
// -- Playback Control Commands -- //
|
||||||
// TODO: relative mode
|
Next,
|
||||||
Add(String, Option<SongPosition>),
|
Pause(Option<bool>),
|
||||||
// TODO: relative mode
|
Play(SongPosition),
|
||||||
AddId(String, Option<SongPosition>),
|
PlayId(SongId),
|
||||||
Clear,
|
Previous,
|
||||||
Delete(OneOrRange),
|
Seek(SongPosition, TimeWithFractions),
|
||||||
DeleteId(SongId),
|
SeekId(SongId, TimeWithFractions),
|
||||||
Move(OneOrRange, SongPosition),
|
SeekCur(SeekMode, TimeWithFractions),
|
||||||
MoveId(SongId, SongPosition),
|
Stop,
|
||||||
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),
|
|
||||||
|
|
||||||
// -- Stored Playlist Commands -- //
|
// -- Queue Commands -- //
|
||||||
ListPlaylist(PlaylistName, Option<WindowRange>),
|
// TODO: relative mode
|
||||||
ListPlaylistInfo(PlaylistName, Option<WindowRange>),
|
Add(String, Option<SongPosition>),
|
||||||
SearchPlaylist(PlaylistName, Filter, Option<WindowRange>),
|
// TODO: relative mode
|
||||||
ListPlaylists,
|
AddId(String, Option<SongPosition>),
|
||||||
Load(PlaylistName, Option<WindowRange>, SongPosition),
|
Clear,
|
||||||
PlaylistAdd(PlaylistName, Uri, SongPosition),
|
Delete(OneOrRange),
|
||||||
PlaylistClear(PlaylistName),
|
DeleteId(SongId),
|
||||||
PlaylistDelete(PlaylistName, OneOrRange),
|
Move(OneOrRange, SongPosition),
|
||||||
PlaylistLength(PlaylistName),
|
MoveId(SongId, SongPosition),
|
||||||
// TODO: which type of range?
|
Playlist,
|
||||||
PlaylistMove(PlaylistName, OneOrRange, SongPosition),
|
PlaylistFind(Filter, Option<Sort>, Option<WindowRange>),
|
||||||
Rename(PlaylistName, PlaylistName),
|
PlaylistId(SongId),
|
||||||
Rm(PlaylistName),
|
PlaylistInfo(OneOrRange),
|
||||||
Save(PlaylistName, Option<SaveMode>),
|
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 -- //
|
// -- Stored Playlist Commands -- //
|
||||||
AlbumArt(Uri, Offset),
|
ListPlaylist(PlaylistName, Option<WindowRange>),
|
||||||
Count(Filter, Option<GroupType>),
|
ListPlaylistInfo(PlaylistName, Option<WindowRange>),
|
||||||
GetFingerprint(Uri),
|
SearchPlaylist(PlaylistName, Filter, Option<WindowRange>),
|
||||||
Find(Filter, Option<Sort>, Option<WindowRange>),
|
ListPlaylists,
|
||||||
FindAdd(Filter, Option<Sort>, Option<WindowRange>, Option<SongPosition>),
|
Load(PlaylistName, Option<WindowRange>, SongPosition),
|
||||||
List(Tag, Filter, Option<GroupType>),
|
PlaylistAdd(PlaylistName, Uri, SongPosition),
|
||||||
#[deprecated]
|
PlaylistClear(PlaylistName),
|
||||||
ListAll(Option<Uri>),
|
PlaylistDelete(PlaylistName, OneOrRange),
|
||||||
#[deprecated]
|
PlaylistLength(PlaylistName),
|
||||||
ListAllInfo(Option<Uri>),
|
// TODO: which type of range?
|
||||||
ListFiles(Uri),
|
PlaylistMove(PlaylistName, OneOrRange, SongPosition),
|
||||||
LsInfo(Option<Uri>),
|
Rename(PlaylistName, PlaylistName),
|
||||||
ReadComments(Uri),
|
Rm(PlaylistName),
|
||||||
ReadPicture(Uri, Offset),
|
Save(PlaylistName, Option<SaveMode>),
|
||||||
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 -- //
|
// -- Music Database Commands -- //
|
||||||
Mount(Option<Path>, Option<Uri>),
|
AlbumArt(Uri, Offset),
|
||||||
Unmount(Path),
|
Count(Filter, Option<GroupType>),
|
||||||
ListMounts,
|
GetFingerprint(Uri),
|
||||||
ListNeighbors,
|
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 -- //
|
// -- Mount and Neighbor Commands -- //
|
||||||
StickerGet(StickerType, Uri, String),
|
Mount(Option<Path>, Option<Uri>),
|
||||||
StickerSet(StickerType, Uri, String, String),
|
Unmount(Path),
|
||||||
StickerDelete(StickerType, Uri, String),
|
ListMounts,
|
||||||
StickerList(StickerType, Uri),
|
ListNeighbors,
|
||||||
StickerFind(StickerType, Uri, String, Option<Sort>, Option<WindowRange>),
|
|
||||||
StickerFindValue(StickerType, Uri, String, String, Option<Sort>, Option<WindowRange>),
|
|
||||||
StickerNames,
|
|
||||||
StickerTypes,
|
|
||||||
StickerNamesTypes(Option<StickerType>),
|
|
||||||
|
|
||||||
// -- Connection Commands -- //
|
// -- Sticker Commands -- //
|
||||||
Close,
|
StickerGet(StickerType, Uri, String),
|
||||||
Kill,
|
StickerSet(StickerType, Uri, String, String),
|
||||||
Password(String),
|
StickerDelete(StickerType, Uri, String),
|
||||||
Ping,
|
StickerList(StickerType, Uri),
|
||||||
BinaryLimit(u64),
|
StickerFind(StickerType, Uri, String, Option<Sort>, Option<WindowRange>),
|
||||||
TagTypes,
|
StickerFindValue(
|
||||||
TagTypesDisable(Vec<Tag>),
|
StickerType,
|
||||||
TagTypesEnable(Vec<Tag>),
|
Uri,
|
||||||
TagTypesClear,
|
String,
|
||||||
TagTypesAll,
|
String,
|
||||||
TagTypesAvailable,
|
Option<Sort>,
|
||||||
TagTypesReset(Vec<Tag>),
|
Option<WindowRange>,
|
||||||
Protocol,
|
),
|
||||||
ProtocolDisable(Vec<Feature>),
|
StickerNames,
|
||||||
ProtocolEnable(Vec<Feature>),
|
StickerTypes,
|
||||||
ProtocolClear,
|
StickerNamesTypes(Option<StickerType>),
|
||||||
ProtocolAll,
|
|
||||||
ProtocolAvailable,
|
|
||||||
|
|
||||||
// -- Partition Commands -- //
|
// -- Connection Commands -- //
|
||||||
Partition(PartitionName),
|
Close,
|
||||||
ListPartitions,
|
Kill,
|
||||||
NewPartition(PartitionName),
|
Password(String),
|
||||||
DelPartition(PartitionName),
|
Ping,
|
||||||
MoveOutput(String),
|
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 -- //
|
// -- Partition Commands -- //
|
||||||
DisableOutput(AudioOutputId),
|
Partition(PartitionName),
|
||||||
EnableOutput(AudioOutputId),
|
ListPartitions,
|
||||||
ToggleOutput(AudioOutputId),
|
NewPartition(PartitionName),
|
||||||
Outputs,
|
DelPartition(PartitionName),
|
||||||
OutputSet(AudioOutputId, String, String),
|
MoveOutput(String),
|
||||||
|
|
||||||
// -- Reflection Commands -- //
|
// -- Audio Output Commands -- //
|
||||||
Config,
|
DisableOutput(AudioOutputId),
|
||||||
Commands,
|
EnableOutput(AudioOutputId),
|
||||||
NotCommands,
|
ToggleOutput(AudioOutputId),
|
||||||
UrlHandlers,
|
Outputs,
|
||||||
Decoders,
|
OutputSet(AudioOutputId, String, String),
|
||||||
|
|
||||||
// -- Client to Client Commands -- //
|
// -- Reflection Commands -- //
|
||||||
Subscribe(ChannelName),
|
Config,
|
||||||
Unsubscribe(ChannelName),
|
Commands,
|
||||||
Channels,
|
NotCommands,
|
||||||
ReadMessages,
|
UrlHandlers,
|
||||||
SendMessage(ChannelName, String),
|
Decoders,
|
||||||
|
|
||||||
|
// -- Client to Client Commands -- //
|
||||||
|
Subscribe(ChannelName),
|
||||||
|
Unsubscribe(ChannelName),
|
||||||
|
Channels,
|
||||||
|
ReadMessages,
|
||||||
|
SendMessage(ChannelName, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Vec<u8>> for Request {
|
// impl From<Request> for Vec<u8> {
|
||||||
fn into(self) -> Vec<u8> {
|
// fn from(val: Request) -> Self {
|
||||||
todo!()
|
// 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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum SaveMode {
|
|
||||||
Create,
|
|
||||||
Append,
|
|
||||||
Replace,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum SeekMode {
|
pub enum SeekMode {
|
||||||
Relative,
|
Relative,
|
||||||
RelativeReverse,
|
RelativeReverse,
|
||||||
Absolute,
|
Absolute,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum SubSystem {
|
pub enum ReplayGainModeMode {
|
||||||
/// The song database has been modified after update.
|
Off,
|
||||||
Database,
|
Track,
|
||||||
/// A database update has started or finished. If the database was modified during the update, the database event is also emitted.
|
Album,
|
||||||
Update,
|
Auto,
|
||||||
/// A stored playlist has been modified, renamed, created or deleted
|
}
|
||||||
StoredPlaylist,
|
impl FromStr for ReplayGainModeMode {
|
||||||
/// The queue (i.e. the current playlist) has been modified
|
type Err = ();
|
||||||
Playlist,
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
/// The player has been started, stopped or seeked or tags of the currently playing song have changed (e.g. received from stream)
|
match s {
|
||||||
Player,
|
"off" => Ok(Self::Off),
|
||||||
/// The volume has been changed
|
"track" => Ok(Self::Track),
|
||||||
Mixer,
|
"album" => Ok(Self::Album),
|
||||||
/// An audio output has been added, removed or modified (e.g. renamed, enabled or disabled)
|
"auto" => Ok(Self::Auto),
|
||||||
Output,
|
_ => Err(()),
|
||||||
/// 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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
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)]
|
|
||||||
pub struct Volume(u32);
|
pub struct Volume(u32);
|
||||||
impl Volume {
|
impl Volume {
|
||||||
pub fn new(volume: u32) -> Result<Self, ()> {
|
pub fn new(volume: u32) -> Result<Self, ()> {
|
||||||
match volume {
|
match volume {
|
||||||
0..=100 => Ok(Self(volume)),
|
0..=100 => Ok(Self(volume)),
|
||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
impl Into<u32> for Volume {
|
impl From<Volume> for u32 {
|
||||||
fn into(self) -> u32 {
|
fn from(val: Volume) -> Self {
|
||||||
self.0
|
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
|
// TODO: fill out
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum GroupType {
|
pub enum GroupType {
|
||||||
Artist,
|
Artist,
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
Date,
|
Date,
|
||||||
Genre,
|
Genre,
|
||||||
Track,
|
Track,
|
||||||
Composer,
|
Composer,
|
||||||
Performer,
|
Performer,
|
||||||
Conductor,
|
Conductor,
|
||||||
Comment,
|
Comment,
|
||||||
Disc,
|
Disc,
|
||||||
Filename,
|
Filename,
|
||||||
Any,
|
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
|
// See https://github.com/MusicPlayerDaemon/MPD/blob/7774c3369e1484dc5dec6d7d9572e0a57e9c5302/src/command/AllCommands.cxx#L67-L209
|
||||||
pub enum Response {
|
pub enum Response {
|
||||||
Ok,
|
Ok,
|
||||||
GenericError(String),
|
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