split property parsing from event parsing:
Build and test / build (pull_request) Successful in 1m55s Details
Build and test / check (pull_request) Successful in 1m52s Details
Build and test / docs (pull_request) Successful in 2m21s Details
Build and test / test (pull_request) Successful in 4m9s Details
Build and test / check (push) Successful in 1m55s Details
Build and test / build (push) Successful in 1m57s Details
Build and test / docs (push) Successful in 2m43s Details
Build and test / test (push) Successful in 5m33s Details

High-level properties are now optional, considering there are about a
thousand of them to parse. The high-level properties are a few chosen
ones that I suspect might be useful for most people, with catch-all enum
variants for the less common ones.
This commit is contained in:
Oystein Kristoffer Tveit 2024-05-02 20:59:30 +02:00
parent f5ca2ebde9
commit 7eec34ce00
Signed by: oysteikt
GPG Key ID: 9F2F7D8250F35146
8 changed files with 475 additions and 192 deletions

View File

@ -75,6 +75,7 @@ pub enum MpvDataType {
Double(f64), Double(f64),
HashMap(HashMap<String, MpvDataType>), HashMap(HashMap<String, MpvDataType>),
Null, Null,
MinusOne,
Playlist(Playlist), Playlist(Playlist),
String(String), String(String),
Usize(usize), Usize(usize),

View File

@ -1,29 +1,67 @@
//! JSON parsing logic for events from [`MpvIpc`](crate::ipc::MpvIpc). //! JSON parsing logic for events from [`MpvIpc`](crate::ipc::MpvIpc).
use std::collections::HashMap; use std::str::FromStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use crate::{ipc::MpvIpcEvent, Error, ErrorCode, MpvDataType}; use crate::{ipc::MpvIpcEvent, message_parser::json_to_value, Error, ErrorCode, MpvDataType};
/// All possible properties that can be observed through the event system.
///
/// Not all properties are guaranteed to be implemented.
/// If something is missing, please open an issue.
///
/// Otherwise, the property will be returned as a `Property::Unknown` variant.
///
/// See <https://mpv.io/manual/master/#properties> for
/// the upstream list of properties.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Property { #[serde(rename_all = "kebab-case")]
Path(Option<String>), pub enum EventEndFileReason {
Pause(bool), Eof,
PlaybackTime(Option<f64>), Stop,
Duration(Option<f64>), Quit,
Metadata(Option<HashMap<String, MpvDataType>>), Error,
Unknown { name: String, data: MpvDataType }, Redirect,
Unknown,
Unimplemented(String),
}
impl FromStr for EventEndFileReason {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"eof" => Ok(EventEndFileReason::Eof),
"stop" => Ok(EventEndFileReason::Stop),
"quit" => Ok(EventEndFileReason::Quit),
"error" => Ok(EventEndFileReason::Error),
"redirect" => Ok(EventEndFileReason::Redirect),
reason => Ok(EventEndFileReason::Unimplemented(reason.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum EventLogMessageLevel {
Info,
Warn,
Error,
Fatal,
Verbose,
Debug,
Trace,
Unimplemented(String),
}
impl FromStr for EventLogMessageLevel {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"info" => Ok(EventLogMessageLevel::Info),
"warn" => Ok(EventLogMessageLevel::Warn),
"error" => Ok(EventLogMessageLevel::Error),
"fatal" => Ok(EventLogMessageLevel::Fatal),
"verbose" => Ok(EventLogMessageLevel::Verbose),
"debug" => Ok(EventLogMessageLevel::Debug),
"trace" => Ok(EventLogMessageLevel::Trace),
level => Ok(EventLogMessageLevel::Unimplemented(level.to_string())),
}
}
} }
/// All possible events that can be sent by mpv. /// All possible events that can be sent by mpv.
@ -36,29 +74,90 @@ pub enum Property {
/// See <https://mpv.io/manual/master/#list-of-events> for /// See <https://mpv.io/manual/master/#list-of-events> for
/// the upstream list of events. /// the upstream list of events.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Event { pub enum Event {
Shutdown, StartFile {
StartFile, playlist_entry_id: usize,
EndFile, },
EndFile {
reason: EventEndFileReason,
playlist_entry_id: usize,
file_error: Option<String>,
playlist_insert_id: Option<usize>,
playlist_insert_num_entries: Option<usize>,
},
FileLoaded, FileLoaded,
TracksChanged,
TrackSwitched,
Idle,
Pause,
Unpause,
Tick,
VideoReconfig,
AudioReconfig,
MetadataUpdate,
Seek, Seek,
PlaybackRestart, PlaybackRestart,
PropertyChange { id: usize, property: Property }, Shutdown,
LogMessage {
prefix: String,
level: EventLogMessageLevel,
text: String,
},
Hook {
hook_id: usize,
},
GetPropertyReply,
SetPropertyReply,
CommandReply {
result: String,
},
ClientMessage {
args: Vec<String>,
},
VideoReconfig,
AudioReconfig,
PropertyChange {
id: usize,
name: String,
data: MpvDataType,
},
EventQueueOverflow,
None,
/// Deprecated since mpv v0.33.0
Idle,
/// Deprecated since mpv v0.31.0
Tick,
/// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
TracksChanged,
/// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
TrackSwitched,
/// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
Pause,
/// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
Unpause,
/// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
MetadataUpdate,
/// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
ChapterChange, ChapterChange,
ClientMessage { args: Vec<String> },
Unimplemented, /// Deprecated since mpv v0.7.0, removed in mpv v0.35.0
ScriptInputDispatch,
/// Catch-all for unimplemented events
Unimplemented(Map<String, Value>),
} }
// NOTE: I have not been able to test all of these events,
// so some of the parsing logic might be incorrect.
// In particular, I have not been able to make mpv
// produce any of the commented out events, and since
// the documentation for the most part just says
// "See C API", I have not pursued this further.
//
// If you need this, please open an issue or a PR.
/// Parse a highlevel [`Event`] objects from json. /// Parse a highlevel [`Event`] objects from json.
#[allow(deprecated)]
pub(crate) fn parse_event(raw_event: MpvIpcEvent) -> Result<Event, Error> { pub(crate) fn parse_event(raw_event: MpvIpcEvent) -> Result<Event, Error> {
let MpvIpcEvent(event) = raw_event; let MpvIpcEvent(event) = raw_event;
@ -73,46 +172,129 @@ pub(crate) fn parse_event(raw_event: MpvIpcEvent) -> Result<Event, Error> {
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?; .ok_or(Error(ErrorCode::ValueDoesNotContainString))?;
match event_name { match event_name {
"shutdown" => Ok(Event::Shutdown), "start-file" => parse_start_file(event),
"start-file" => Ok(Event::StartFile), "end-file" => parse_end_file(event),
"end-file" => Ok(Event::EndFile),
"file-loaded" => Ok(Event::FileLoaded), "file-loaded" => Ok(Event::FileLoaded),
"tracks-changed" => Ok(Event::TracksChanged),
"track-switched" => Ok(Event::TrackSwitched),
"idle" => Ok(Event::Idle),
"pause" => Ok(Event::Pause),
"unpause" => Ok(Event::Unpause),
"tick" => Ok(Event::Tick),
"video-reconfig" => Ok(Event::VideoReconfig),
"audio-reconfig" => Ok(Event::AudioReconfig),
"metadata-update" => Ok(Event::MetadataUpdate),
"seek" => Ok(Event::Seek), "seek" => Ok(Event::Seek),
"playback-restart" => Ok(Event::PlaybackRestart), "playback-restart" => Ok(Event::PlaybackRestart),
"property-change" => parse_event_property(event) "shutdown" => Ok(Event::Shutdown),
.map(|(id, property)| Event::PropertyChange { id, property }), "log-message" => parse_log_message(event),
"hook" => parse_hook(event),
// "get-property-reply" =>
// "set-property-reply" =>
// "command-reply" =>
"client-message" => parse_client_message(event),
"video-reconfig" => Ok(Event::VideoReconfig),
"audio-reconfig" => Ok(Event::AudioReconfig),
"property-change" => parse_property_change(event),
"tick" => Ok(Event::Tick),
"idle" => Ok(Event::Idle),
"tracks-changed" => Ok(Event::TracksChanged),
"track-switched" => Ok(Event::TrackSwitched),
"pause" => Ok(Event::Pause),
"unpause" => Ok(Event::Unpause),
"metadata-update" => Ok(Event::MetadataUpdate),
"chapter-change" => Ok(Event::ChapterChange), "chapter-change" => Ok(Event::ChapterChange),
"client-message" => { _ => Ok(Event::Unimplemented(event.to_owned())),
let args = event
.get("args")
.ok_or(Error(ErrorCode::MissingValue))?
.as_array()
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?
.iter()
.map(|arg| {
arg.as_str()
.ok_or(Error(ErrorCode::ValueDoesNotContainString))
.map(|s| s.to_string())
})
.collect::<Result<Vec<String>, Error>>()?;
Ok(Event::ClientMessage { args })
}
_ => Ok(Event::Unimplemented),
} }
}) })
} }
/// Parse a highlevel [`Property`] object from json, used for [`Event::PropertyChange`]. fn parse_start_file(event: &Map<String, Value>) -> Result<Event, Error> {
fn parse_event_property(event: &Map<String, Value>) -> Result<(usize, Property), Error> { let playlist_entry_id = event
.get("playlist_entry_id")
.ok_or(Error(ErrorCode::MissingValue))?
.as_u64()
.ok_or(Error(ErrorCode::ValueDoesNotContainUsize))? as usize;
Ok(Event::StartFile { playlist_entry_id })
}
fn parse_end_file(event: &Map<String, Value>) -> Result<Event, Error> {
let reason = event
.get("reason")
.ok_or(Error(ErrorCode::MissingValue))?
.as_str()
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?;
let playlist_entry_id = event
.get("playlist_entry_id")
.ok_or(Error(ErrorCode::MissingValue))?
.as_u64()
.ok_or(Error(ErrorCode::ValueDoesNotContainUsize))? as usize;
let file_error = event
.get("file_error")
.and_then(|v| v.as_str().map(|s| s.to_string()));
let playlist_insert_id = event
.get("playlist_insert_id")
.and_then(|v| v.as_u64().map(|u| u as usize));
let playlist_insert_num_entries = event
.get("playlist_insert_num_entries")
.and_then(|v| v.as_u64().map(|u| u as usize));
Ok(Event::EndFile {
reason: reason
.parse()
.unwrap_or(EventEndFileReason::Unimplemented(reason.to_string())),
playlist_entry_id,
file_error,
playlist_insert_id,
playlist_insert_num_entries,
})
}
fn parse_log_message(event: &Map<String, Value>) -> Result<Event, Error> {
let prefix = event
.get("prefix")
.ok_or(Error(ErrorCode::MissingValue))?
.as_str()
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?
.to_string();
let level = event
.get("level")
.ok_or(Error(ErrorCode::MissingValue))?
.as_str()
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?;
let text = event
.get("text")
.ok_or(Error(ErrorCode::MissingValue))?
.as_str()
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?
.to_string();
Ok(Event::LogMessage {
prefix,
level: level
.parse()
.unwrap_or(EventLogMessageLevel::Unimplemented(level.to_string())),
text,
})
}
fn parse_hook(event: &Map<String, Value>) -> Result<Event, Error> {
let hook_id = event
.get("hook_id")
.ok_or(Error(ErrorCode::MissingValue))?
.as_u64()
.ok_or(Error(ErrorCode::ValueDoesNotContainUsize))? as usize;
Ok(Event::Hook { hook_id })
}
fn parse_client_message(event: &Map<String, Value>) -> Result<Event, Error> {
let args = event
.get("args")
.ok_or(Error(ErrorCode::MissingValue))?
.as_array()
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?
.iter()
.map(|arg| {
arg.as_str()
.ok_or(Error(ErrorCode::ValueDoesNotContainString))
.map(|s| s.to_string())
})
.collect::<Result<Vec<String>, Error>>()?;
Ok(Event::ClientMessage { args })
}
fn parse_property_change(event: &Map<String, Value>) -> Result<Event, Error> {
let id = event let id = event
.get("id") .get("id")
.ok_or(Error(ErrorCode::MissingValue))? .ok_or(Error(ErrorCode::MissingValue))?
@ -123,38 +305,14 @@ fn parse_event_property(event: &Map<String, Value>) -> Result<(usize, Property),
.ok_or(Error(ErrorCode::MissingValue))? .ok_or(Error(ErrorCode::MissingValue))?
.as_str() .as_str()
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?; .ok_or(Error(ErrorCode::ValueDoesNotContainString))?;
let data = event
.get("data")
.ok_or(Error(ErrorCode::MissingValue))?
.clone();
match property_name { Ok(Event::PropertyChange {
"path" => { id,
let path = event name: property_name.to_string(),
.get("data") data: json_to_value(&data)?,
.ok_or(Error(ErrorCode::MissingValue))? })
.as_str()
.map(|s| s.to_string());
Ok((id, Property::Path(path)))
}
"pause" => {
let pause = event
.get("data")
.ok_or(Error(ErrorCode::MissingValue))?
.as_bool()
.ok_or(Error(ErrorCode::ValueDoesNotContainBool))?;
Ok((id, Property::Pause(pause)))
}
// TODO: missing cases
_ => {
let data = event
.get("data")
.ok_or(Error(ErrorCode::MissingValue))?
.clone();
Ok((
id,
Property::Unknown {
name: property_name.to_string(),
// TODO: fix
data: MpvDataType::Double(data.as_f64().unwrap_or(0.0)),
},
))
}
}
} }

View File

@ -0,0 +1,171 @@
//! JSON parsing logic for properties returned in [`Event::PropertyChange`]
//!
//! This module is used to parse the json data from the `data` field of the
//! [`Event::PropertyChange`] variant. Mpv has about 1000 different properties
//! as of `v0.38.0`, so this module will only implement the most common ones.
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{Error, ErrorCode, Event, MpvDataType, PlaylistEntry};
/// All possible properties that can be observed through the event system.
///
/// Not all properties are guaranteed to be implemented.
/// If something is missing, please open an issue.
///
/// Otherwise, the property will be returned as a `Property::Unknown` variant.
///
/// See <https://mpv.io/manual/master/#properties> for
/// the upstream list of properties.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Property {
Path(Option<String>),
Pause(bool),
PlaybackTime(Option<f64>),
Duration(Option<f64>),
Metadata(Option<HashMap<String, MpvDataType>>),
Playlist(Vec<PlaylistEntry>),
PlaylistPos(Option<usize>),
LoopFile(LoopProperty),
LoopPlaylist(LoopProperty),
Speed(f64),
Volume(f64),
Mute(bool),
Unknown { name: String, data: MpvDataType },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LoopProperty {
N(usize),
Inf,
No,
}
/// Parse a highlevel [`Property`] object from json, used for [`Event::PropertyChange`].
pub fn parse_event_property(event: Event) -> Result<(usize, Property), Error> {
let (id, name, data) = match event {
Event::PropertyChange { id, name, data } => (id, name, data),
// TODO: return proper error
_ => {
panic!("Event is not a PropertyChange event")
}
};
match name.as_str() {
"path" => {
let path = match data {
MpvDataType::String(s) => Some(s),
MpvDataType::Null => None,
_ => return Err(Error(ErrorCode::ValueDoesNotContainString)),
};
Ok((id, Property::Path(path)))
}
"pause" => {
let pause = match data {
MpvDataType::Bool(b) => b,
_ => return Err(Error(ErrorCode::ValueDoesNotContainBool)),
};
Ok((id, Property::Pause(pause)))
}
"playback-time" => {
let playback_time = match data {
MpvDataType::Double(d) => Some(d),
MpvDataType::Null => None,
_ => return Err(Error(ErrorCode::ValueDoesNotContainF64)),
};
Ok((id, Property::PlaybackTime(playback_time)))
}
"duration" => {
let duration = match data {
MpvDataType::Double(d) => Some(d),
MpvDataType::Null => None,
_ => return Err(Error(ErrorCode::ValueDoesNotContainF64)),
};
Ok((id, Property::Duration(duration)))
}
"metadata" => {
let metadata = match data {
MpvDataType::HashMap(m) => Some(m),
MpvDataType::Null => None,
_ => return Err(Error(ErrorCode::ValueDoesNotContainHashMap)),
};
Ok((id, Property::Metadata(metadata)))
}
// "playlist" => {
// let playlist = match data {
// MpvDataType::Array(a) => json_array_to_playlist(&a),
// MpvDataType::Null => Vec::new(),
// _ => return Err(Error(ErrorCode::ValueDoesNotContainPlaylist)),
// };
// Ok((id, Property::Playlist(playlist)))
// }
"playlist-pos" => {
let playlist_pos = match data {
MpvDataType::Usize(u) => Some(u),
MpvDataType::MinusOne => None,
MpvDataType::Null => None,
_ => return Err(Error(ErrorCode::ValueDoesNotContainUsize)),
};
Ok((id, Property::PlaylistPos(playlist_pos)))
}
"loop-file" => {
let loop_file = match data {
MpvDataType::Usize(n) => LoopProperty::N(n),
MpvDataType::Bool(b) => match b {
true => LoopProperty::Inf,
false => LoopProperty::No,
},
MpvDataType::String(s) => match s.as_str() {
"inf" => LoopProperty::Inf,
"no" => LoopProperty::No,
_ => return Err(Error(ErrorCode::ValueDoesNotContainString)),
},
_ => return Err(Error(ErrorCode::ValueDoesNotContainString)),
};
Ok((id, Property::LoopFile(loop_file)))
}
"loop-playlist" => {
let loop_playlist = match data {
MpvDataType::Usize(n) => LoopProperty::N(n),
MpvDataType::Bool(b) => match b {
true => LoopProperty::Inf,
false => LoopProperty::No,
},
MpvDataType::String(s) => match s.as_str() {
"inf" => LoopProperty::Inf,
"no" => LoopProperty::No,
_ => return Err(Error(ErrorCode::ValueDoesNotContainString)),
},
_ => return Err(Error(ErrorCode::ValueDoesNotContainString)),
};
Ok((id, Property::LoopPlaylist(loop_playlist)))
}
"speed" => {
let speed = match data {
MpvDataType::Double(d) => d,
_ => return Err(Error(ErrorCode::ValueDoesNotContainF64)),
};
Ok((id, Property::Speed(speed)))
}
"volume" => {
let volume = match data {
MpvDataType::Double(d) => d,
_ => return Err(Error(ErrorCode::ValueDoesNotContainF64)),
};
Ok((id, Property::Volume(volume)))
}
"mute" => {
let mute = match data {
MpvDataType::Bool(b) => b,
_ => return Err(Error(ErrorCode::ValueDoesNotContainBool)),
};
Ok((id, Property::Mute(mute)))
}
// TODO: add missing cases
_ => Ok((id, Property::Unknown { name, data })),
}
}

View File

@ -51,9 +51,6 @@ impl MpvIpc {
} }
pub(crate) async fn send_command(&mut self, command: &[Value]) -> Result<Option<Value>, Error> { pub(crate) async fn send_command(&mut self, command: &[Value]) -> Result<Option<Value>, Error> {
// let lock = self.socket_lock.lock().await;
// START CRITICAL SECTION
let ipc_command = json!({ "command": command }); let ipc_command = json!({ "command": command });
let ipc_command_str = serde_json::to_string(&ipc_command) let ipc_command_str = serde_json::to_string(&ipc_command)
.map_err(|why| Error(ErrorCode::JsonParseError(why.to_string())))?; .map_err(|why| Error(ErrorCode::JsonParseError(why.to_string())))?;
@ -87,8 +84,6 @@ impl MpvIpc {
break parsed_response; break parsed_response;
} }
}; };
// END CRITICAL SECTION
// mem::drop(lock);
log::trace!("Received response: {:?}", response); log::trace!("Received response: {:?}", response);

View File

@ -3,6 +3,7 @@
mod core_api; mod core_api;
mod error; mod error;
mod event_parser; mod event_parser;
mod event_property_parser;
mod highlevel_api_extension; mod highlevel_api_extension;
mod ipc; mod ipc;
mod message_parser; mod message_parser;
@ -10,4 +11,5 @@ mod message_parser;
pub use core_api::*; pub use core_api::*;
pub use error::*; pub use error::*;
pub use event_parser::*; pub use event_parser::*;
pub use event_property_parser::*;
pub use highlevel_api_extension::*; pub use highlevel_api_extension::*;

View File

@ -91,45 +91,36 @@ impl TypeHandler for Vec<PlaylistEntry> {
} }
} }
pub(crate) fn json_to_value(value: &Value) -> Result<MpvDataType, Error> {
match value {
Value::Array(array) => Ok(MpvDataType::Array(json_array_to_vec(array))),
Value::Bool(b) => Ok(MpvDataType::Bool(*b)),
Value::Number(n) => {
if n.is_i64() && n.as_i64().unwrap() == -1 {
Ok(MpvDataType::MinusOne)
} else if n.is_u64() {
Ok(MpvDataType::Usize(n.as_u64().unwrap() as usize))
} else if n.is_f64() {
Ok(MpvDataType::Double(n.as_f64().unwrap()))
} else {
// TODO: proper error handling
panic!("Unexpected number type");
}
}
Value::Object(map) => Ok(MpvDataType::HashMap(json_map_to_hashmap(map))),
Value::String(s) => Ok(MpvDataType::String(s.to_string())),
Value::Null => Ok(MpvDataType::Null),
}
}
pub(crate) fn json_map_to_hashmap( pub(crate) fn json_map_to_hashmap(
map: &serde_json::map::Map<String, Value>, map: &serde_json::map::Map<String, Value>,
) -> HashMap<String, MpvDataType> { ) -> HashMap<String, MpvDataType> {
let mut output_map: HashMap<String, MpvDataType> = HashMap::new(); let mut output_map: HashMap<String, MpvDataType> = HashMap::new();
for (ref key, value) in map.iter() { for (ref key, value) in map.iter() {
match *value { // TODO: proper error handling
Value::Array(ref array) => { if let Ok(value) = json_to_value(value) {
output_map.insert( output_map.insert(key.to_string(), value);
key.to_string(),
MpvDataType::Array(json_array_to_vec(array)),
);
}
Value::Bool(ref b) => {
output_map.insert(key.to_string(), MpvDataType::Bool(*b));
}
Value::Number(ref n) => {
if n.is_u64() {
output_map.insert(
key.to_string(),
MpvDataType::Usize(n.as_u64().unwrap() as usize),
);
} else if n.is_f64() {
output_map.insert(key.to_string(), MpvDataType::Double(n.as_f64().unwrap()));
} else {
panic!("unimplemented number");
}
}
Value::String(ref s) => {
output_map.insert(key.to_string(), MpvDataType::String(s.to_string()));
}
Value::Object(ref m) => {
output_map.insert(
key.to_string(),
MpvDataType::HashMap(json_map_to_hashmap(m)),
);
}
Value::Null => {
unimplemented!();
}
} }
} }
output_map output_map
@ -138,24 +129,8 @@ pub(crate) fn json_map_to_hashmap(
pub(crate) fn json_array_to_vec(array: &[Value]) -> Vec<MpvDataType> { pub(crate) fn json_array_to_vec(array: &[Value]) -> Vec<MpvDataType> {
array array
.iter() .iter()
.map(|entry| match entry { // TODO: proper error handling
Value::Array(a) => MpvDataType::Array(json_array_to_vec(a)), .filter_map(|entry| json_to_value(entry).ok())
Value::Bool(b) => MpvDataType::Bool(*b),
Value::Number(n) => {
if n.is_u64() {
MpvDataType::Usize(n.as_u64().unwrap() as usize)
} else if n.is_f64() {
MpvDataType::Double(n.as_f64().unwrap())
} else {
panic!("unimplemented number");
}
}
Value::Object(ref o) => MpvDataType::HashMap(json_map_to_hashmap(o)),
Value::String(s) => MpvDataType::String(s.to_owned()),
Value::Null => {
unimplemented!();
}
})
.collect() .collect()
} }
@ -198,6 +173,8 @@ mod test {
"bool": true, "bool": true,
"double": 1.0, "double": 1.0,
"usize": 1, "usize": 1,
"minus_one": -1,
"null": null,
"string": "string", "string": "string",
"object": { "object": {
"key": "value" "key": "value"
@ -216,6 +193,8 @@ mod test {
expected.insert("bool".to_string(), MpvDataType::Bool(true)); expected.insert("bool".to_string(), MpvDataType::Bool(true));
expected.insert("double".to_string(), MpvDataType::Double(1.0)); expected.insert("double".to_string(), MpvDataType::Double(1.0));
expected.insert("usize".to_string(), MpvDataType::Usize(1)); expected.insert("usize".to_string(), MpvDataType::Usize(1));
expected.insert("minus_one".to_string(), MpvDataType::MinusOne);
expected.insert("null".to_string(), MpvDataType::Null);
expected.insert( expected.insert(
"string".to_string(), "string".to_string(),
MpvDataType::String("string".to_string()), MpvDataType::String("string".to_string()),
@ -231,18 +210,6 @@ mod test {
assert_eq!(json_map_to_hashmap(json.as_object().unwrap()), expected); assert_eq!(json_map_to_hashmap(json.as_object().unwrap()), expected);
} }
#[test]
#[should_panic]
fn test_json_map_to_hashmap_fail_on_null() {
json_map_to_hashmap(
json!({
"null": null
})
.as_object()
.unwrap(),
);
}
#[test] #[test]
fn test_json_array_to_vec() { fn test_json_array_to_vec() {
let json = json!([ let json = json!([
@ -250,6 +217,8 @@ mod test {
true, true,
1.0, 1.0,
1, 1,
-1,
null,
"string", "string",
{ {
"key": "value" "key": "value"
@ -268,6 +237,8 @@ mod test {
MpvDataType::Bool(true), MpvDataType::Bool(true),
MpvDataType::Double(1.0), MpvDataType::Double(1.0),
MpvDataType::Usize(1), MpvDataType::Usize(1),
MpvDataType::MinusOne,
MpvDataType::Null,
MpvDataType::String("string".to_string()), MpvDataType::String("string".to_string()),
MpvDataType::HashMap(HashMap::from([( MpvDataType::HashMap(HashMap::from([(
"key".to_string(), "key".to_string(),
@ -278,12 +249,6 @@ mod test {
assert_eq!(json_array_to_vec(json.as_array().unwrap()), expected); assert_eq!(json_array_to_vec(json.as_array().unwrap()), expected);
} }
#[test]
#[should_panic]
fn test_json_array_to_vec_fail_on_null() {
json_array_to_vec(json!([null]).as_array().unwrap().as_slice());
}
#[test] #[test]
fn test_json_array_to_playlist() { fn test_json_array_to_playlist() {
let json = json!([ let json = json!([

View File

@ -1,14 +1,12 @@
use std::panic; use std::panic;
use futures::{stream::StreamExt, SinkExt}; use futures::{stream::StreamExt, SinkExt};
use mpvipc::{Mpv, MpvDataType, MpvExt, Property}; use mpvipc::{parse_event_property, Mpv, MpvDataType, MpvExt, Property};
use serde_json::json; use serde_json::json;
use test_log::test; use test_log::test;
use tokio::{net::UnixStream, task::JoinHandle}; use tokio::{net::UnixStream, task::JoinHandle};
use tokio_util::codec::{Framed, LinesCodec, LinesCodecError}; use tokio_util::codec::{Framed, LinesCodec, LinesCodecError};
use mpvipc::Event;
fn test_socket( fn test_socket(
answers: Vec<(bool, String)>, answers: Vec<(bool, String)>,
) -> (UnixStream, JoinHandle<Result<(), LinesCodecError>>) { ) -> (UnixStream, JoinHandle<Result<(), LinesCodecError>>) {
@ -53,19 +51,13 @@ async fn test_observe_event_successful() {
tokio::spawn(async move { tokio::spawn(async move {
let event = mpv2.get_event_stream().await.next().await.unwrap().unwrap(); let event = mpv2.get_event_stream().await.next().await.unwrap().unwrap();
let property = match event { let data = match parse_event_property(event) {
Event::PropertyChange { id, property } => { Ok((_, Property::Unknown { name, data })) => {
assert_eq!(id, 1);
property
}
err => panic!("{:?}", err),
};
let data = match property {
Property::Unknown { name, data } => {
assert_eq!(name, "volume"); assert_eq!(name, "volume");
data data
} }
err => panic!("{:?}", err), Ok((_, property)) => panic!("{:?}", property),
Err(err) => panic!("{:?}", err),
}; };
match data { match data {
MpvDataType::Double(data) => assert_eq!(data, 64.0), MpvDataType::Double(data) => assert_eq!(data, 64.0),

View File

@ -73,11 +73,9 @@ async fn test_events() {
let event_checking_thread = tokio::spawn(async move { let event_checking_thread = tokio::spawn(async move {
loop { loop {
let event = events.next().await.unwrap().unwrap(); let event = events.next().await.unwrap().unwrap();
if let mpvipc::Event::PropertyChange { id, property } = event { if let (1337, property) = mpvipc::parse_event_property(event).unwrap() {
if id == 1337 { assert_eq!(property, mpvipc::Property::Pause(true));
assert_eq!(property, mpvipc::Property::Pause(true)); break;
break;
}
} }
} }
}); });
@ -86,11 +84,12 @@ async fn test_events() {
mpv.set_property("pause", true).await.unwrap(); mpv.set_property("pause", true).await.unwrap();
if let Err(_) = tokio::time::timeout( if tokio::time::timeout(
tokio::time::Duration::from_millis(500), tokio::time::Duration::from_millis(500),
event_checking_thread, event_checking_thread,
) )
.await .await
.is_err()
{ {
panic!("Event checking thread timed out"); panic!("Event checking thread timed out");
} }