Continued development

This commit is contained in:
Oystein Kristoffer Tveit 2024-11-30 01:57:45 +01:00
parent 8293c6e6e5
commit 49e070a41d
Signed by: oysteikt
GPG Key ID: 9F2F7D8250F35146
50 changed files with 2437 additions and 280 deletions

30
Cargo.lock generated
View File

@ -2,13 +2,37 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "empidee"
version = "0.1.0"
dependencies = [
"indoc",
"pretty_assertions",
"serde",
]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.88"
@ -63,3 +87,9 @@ name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"

View File

@ -4,4 +4,8 @@ version = "0.1.0"
edition = "2021"
[dependencies]
serde = "1.0.210"
serde = { version = "1.0.210", features = ["derive"] }
[dev-dependencies]
indoc = "2.0.5"
pretty_assertions = "1.4.1"

196
src/commands.rs Normal file
View 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;

View File

@ -0,0 +1,5 @@
pub mod disableoutput;
pub mod enableoutput;
pub mod outputs;
pub mod outputset;
pub mod toggleoutput;

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View File

@ -0,0 +1,5 @@
pub mod channels;
pub mod readmessages;
pub mod sendmessage;
pub mod subscribe;
pub mod unsubscribe;

View 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 })
}
}

View 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!()
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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;

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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;

View 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(())
}
}

View 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(())
}
}

View 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)
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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)
})?,
})
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View File

@ -0,0 +1,5 @@
pub mod clearerror;
pub mod currentsong;
pub mod idle;
pub mod stats;
pub mod status;

View 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(())
}
}

View 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!()
}
}

View 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(())
}
}

View 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!()
}
}

View 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,
}),
);
}
}

View File

@ -1,6 +1,33 @@
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Tag {
use serde::{Deserialize, Serialize};
pub type SongPosition = u32;
pub type SongId = u32;
pub type Seconds = u32;
pub type TimeWithFractions = f64;
pub type OneOrRange = (SongPosition, Option<SongPosition>);
pub type WindowRange = (Option<SongPosition>, Option<SongPosition>);
pub type Priority = u8;
pub type PlaylistName = String;
pub type Offset = u32;
// TODO: use a proper types
pub type TagName = String;
pub type TagValue = String;
pub type Uri = String;
pub type Path = String;
pub type Filter = String;
pub type Sort = String;
pub type Version = String;
pub type Feature = String;
pub type PartitionName = String;
pub type AudioOutputId = String;
pub type ChannelName = String;
pub type StickerType = String;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Tag {
/// The artist name. Its meaning is not well-defined; see “composer” and “performer” for more specific tags.
Artist(String),
/// Same as artist, but for sorting. This usually omits prefixes such as “The”.
@ -25,9 +52,9 @@ pub(crate) enum Tag {
Genre(String),
/// The mood of the audio with a few keywords.
Mood(String),
/// The songs release date. This is usually a 4-digit year.
/// The song's release date. This is usually a 4-digit year.
Date(String),
/// The songs original release date.
/// The song's original release date.
OriginalDate(String),
/// The artist who composed the song.
Composer(String),
@ -75,3 +102,108 @@ pub(crate) enum Tag {
/// Other tags not covered by the above
Other(String, String),
}
/// These are different parts of the canonical MPD server.
/// They are mostly used in the protocol with the `idle` command,
/// signalling that the client is waiting for changes in any, one or multiple of these subsystems.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SubSystem {
/// The song database has been modified after update.
Database,
/// A database update has started or finished. If the database was modified during the update, the database event is also emitted.
Update,
/// A stored playlist has been modified, renamed, created or deleted
StoredPlaylist,
/// The queue (i.e. the current playlist) has been modified
Playlist,
/// The player has been started, stopped or seeked or tags of the currently playing song have changed (e.g. received from stream)
Player,
/// The volume has been changed
Mixer,
/// An audio output has been added, removed or modified (e.g. renamed, enabled or disabled)
Output,
/// Options like repeat, random, crossfade, replay gain
Options,
/// A partition was added, removed or changed
Partition,
/// The sticker database has been modified.
Sticker,
/// A client has subscribed or unsubscribed to a channel
Subscription,
/// A message was received on a channel this client is subscribed to; this event is only emitted when the clients 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
View 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),
}

View File

@ -1,11 +1,15 @@
///! This library contains structs and parsing for the mpd protocol
///! as described in https://mpd.readthedocs.io/en/latest/protocol.html#protocol-overview
///!
///! It does not provide any client or server implementation
//! This library contains structs and parsing for the mpd protocol
//! as described in https://mpd.readthedocs.io/en/latest/protocol.html#protocol-overview
//!
//! It does not provide any client or server implementation
mod commands;
mod common;
mod filter;
mod request;
mod response;
mod server;
pub use request::Request;
pub use response::Response;
pub use server::MPDServer;

View File

@ -1,30 +1,17 @@
type SongPosition = u32;
type SongId = u32;
type Seconds = u32;
type TimeWithFractions = f64;
type OneOrRange = (SongPosition, Option<SongPosition>);
type WindowRange = (Option<SongPosition>, Option<SongPosition>);
type Priority = u8;
type PlaylistName = String;
type Offset = u32;
use std::str::FromStr;
// TODO: use a proper types
type TagName = String;
type TagValue = String;
type Uri = String;
type Path = String;
type Filter = String;
type Sort = String;
type Version = String;
type Tag = String;
type Feature = String;
type PartitionName = String;
type AudioOutputId = String;
type ChannelName = String;
type StickerType = String;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
use crate::common::*;
use crate::commands::*;
// TODO: SingleLineString
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Request {
CommandList(Vec<Request>),
// -- Query Commands -- //
ClearError,
CurrentSong,
@ -33,7 +20,7 @@ pub enum Request {
Stats,
// -- Playback Commands -- //
Consume(ConsumeState),
Consume(BoolOrOneshot),
Crossfade(Seconds),
MixRampDb(f32),
MixRampDelay(Seconds),
@ -41,8 +28,8 @@ pub enum Request {
Repeat(bool),
SetVol(Volume),
GetVol,
Single(SingleState),
ReplayGainMode(ReplayGainMode),
Single(BoolOrOneshot),
ReplayGainMode(ReplayGainModeMode),
ReplayGainStatus,
Volume(Volume),
@ -107,7 +94,12 @@ pub enum Request {
Count(Filter, Option<GroupType>),
GetFingerprint(Uri),
Find(Filter, Option<Sort>, Option<WindowRange>),
FindAdd(Filter, Option<Sort>, Option<WindowRange>, Option<SongPosition>),
FindAdd(
Filter,
Option<Sort>,
Option<WindowRange>,
Option<SongPosition>,
),
List(Tag, Filter, Option<GroupType>),
#[deprecated]
ListAll(Option<Uri>),
@ -118,8 +110,18 @@ pub enum Request {
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>),
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>),
@ -136,7 +138,14 @@ pub enum Request {
StickerDelete(StickerType, Uri, String),
StickerList(StickerType, Uri),
StickerFind(StickerType, Uri, String, Option<Sort>, Option<WindowRange>),
StickerFindValue(StickerType, Uri, String, String, Option<Sort>, Option<WindowRange>),
StickerFindValue(
StickerType,
Uri,
String,
String,
Option<Sort>,
Option<WindowRange>,
),
StickerNames,
StickerTypes,
StickerNamesTypes(Option<StickerType>),
@ -190,84 +199,58 @@ pub enum Request {
SendMessage(ChannelName, String),
}
impl Into<Vec<u8>> for Request {
fn into(self) -> Vec<u8> {
todo!()
}
}
// impl From<Request> for Vec<u8> {
// fn from(val: Request) -> Self {
// todo!()
// }
// }
#[derive(Debug, Clone, PartialEq, Eq)]
#[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 SeekMode {
Relative,
RelativeReverse,
Absolute,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubSystem {
/// The song database has been modified after update.
Database,
/// A database update has started or finished. If the database was modified during the update, the database event is also emitted.
Update,
/// A stored playlist has been modified, renamed, created or deleted
StoredPlaylist,
/// The queue (i.e. the current playlist) has been modified
Playlist,
/// The player has been started, stopped or seeked or tags of the currently playing song have changed (e.g. received from stream)
Player,
/// The volume has been changed
Mixer,
/// An audio output has been added, removed or modified (e.g. renamed, enabled or disabled)
Output,
/// Options like repeat, random, crossfade, replay gain
Options,
/// A partition was added, removed or changed
Partition,
/// The sticker database has been modified.
Sticker,
/// A client has subscribed or unsubscribed to a channel
Subscription,
/// A message was received on a channel this client is subscribed to; this event is only emitted when the clients 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)]
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 {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReplayGainModeMode {
Off,
Track,
Album,
Auto,
}
impl FromStr for ReplayGainModeMode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"off" => Ok(Self::Off),
"track" => Ok(Self::Track),
"album" => Ok(Self::Album),
"auto" => Ok(Self::Auto),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Volume(u32);
impl Volume {
pub fn new(volume: u32) -> Result<Self, ()> {
@ -277,14 +260,21 @@ impl Volume {
}
}
}
impl Into<u32> for Volume {
fn into(self) -> u32 {
self.0
impl From<Volume> for u32 {
fn from(val: Volume) -> Self {
val.0
}
}
impl FromStr for Volume {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let volume = s.parse().map_err(|_| ())?;
Volume::new(volume)
}
}
// TODO: fill out
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GroupType {
Artist,
Album,
@ -300,3 +290,71 @@ pub enum GroupType {
Filename,
Any,
}
// trait RequestCommand {
// // The command name used within the protocol
// const COMMAND: &'static str;
// // A function to parse the remaining parts of the command, split by whitespace
// fn parse(parts: SplitWhitespace<'_>) -> RequestParserResponse<'_>;
// }
// type RequestParserResponse<'a> = Result<(Request, &'a str), RequestParserError>;
// pub enum RequestParserError {
// SyntaxError(u64, String),
// MissingCommandListEnd(u64),
// NestedCommandList(u64),
// UnexpectedCommandListEnd(u64),
// UnexpectedEOF,
// MissingNewline,
// }
// TODO: upon encountering an error, there should be a function that lets you skip to the next OK,
// and continue execution. Maybe "parse_next_or_skip(&str) -> RequestParserResponse", which
// could skip stuff internally? Or do we want to report back the error with the entire command
// and let the library user decide what to do?
impl Request {
pub fn parse_next(raw: &str) -> RequestParserResult<'_> {
let (line, rest) = raw
.split_once('\n')
.ok_or(RequestParserError::UnexpectedEOF)?;
let mut parts = line.split_whitespace();
match parts
.next()
.ok_or(RequestParserError::SyntaxError(0, line.to_string()))?
.trim()
{
"command_list_begin" => {
let mut commands = Vec::new();
let mut i = 1;
loop {
i += 1;
let (line, rest) = rest
.split_once('\n')
.ok_or(RequestParserError::MissingCommandListEnd(i))?;
match line.trim() {
"command_list_begin" => {
return Err(RequestParserError::NestedCommandList(i))
}
"command_list_end" => {
return Ok((Request::CommandList(commands), rest));
}
input => {
let (command, _) = Request::parse_next(input)?;
commands.push(command);
}
}
}
}
"command_list_end" => Err(RequestParserError::UnexpectedCommandListEnd(0)),
Idle::COMMAND => Idle::parse_request(parts).map(|(req, _)| (req, rest)),
Status::COMMAND => Status::parse_request(parts).map(|(req, _)| (req, rest)),
ClearError::COMMAND => ClearError::parse_request(parts).map(|(req, _)| (req, rest)),
_ => todo!(),
}
}
}

View File

@ -1,5 +1,37 @@
use serde::{Deserialize, Serialize};
// See https://github.com/MusicPlayerDaemon/MPD/blob/7774c3369e1484dc5dec6d7d9572e0a57e9c5302/src/command/AllCommands.cxx#L67-L209
pub enum Response {
Ok,
GenericError(String),
}
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
View 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 clients 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),
// }