242 lines
7.5 KiB
Rust
242 lines
7.5 KiB
Rust
use std::collections::HashMap;
|
|
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, ResponseAttributes,
|
|
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(
|
|
parts: ResponseAttributes<'_>,
|
|
) -> Result<StatusResponse, ResponseParserError> {
|
|
let parts: HashMap<&str, GenericResponseValue> = parts.into();
|
|
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(
|
|
parts: ResponseAttributes<'_>,
|
|
) -> 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,
|
|
}),
|
|
);
|
|
}
|
|
}
|