From 7eec34ce006495ac44ac62527eff39f639433deb Mon Sep 17 00:00:00 2001 From: h7x4 Date: Thu, 2 May 2024 20:59:30 +0200 Subject: [PATCH] split property parsing from event parsing: 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. --- src/core_api.rs | 1 + src/event_parser.rs | 354 +++++++++++++++++++++++++---------- src/event_property_parser.rs | 171 +++++++++++++++++ src/ipc.rs | 5 - src/lib.rs | 2 + src/message_parser.rs | 105 ++++------- tests/events.rs | 18 +- tests/integration.rs | 11 +- 8 files changed, 475 insertions(+), 192 deletions(-) create mode 100644 src/event_property_parser.rs diff --git a/src/core_api.rs b/src/core_api.rs index 9371ed8..797474d 100644 --- a/src/core_api.rs +++ b/src/core_api.rs @@ -75,6 +75,7 @@ pub enum MpvDataType { Double(f64), HashMap(HashMap), Null, + MinusOne, Playlist(Playlist), String(String), Usize(usize), diff --git a/src/event_parser.rs b/src/event_parser.rs index d22e7ab..ebcd736 100644 --- a/src/event_parser.rs +++ b/src/event_parser.rs @@ -1,29 +1,67 @@ //! JSON parsing logic for events from [`MpvIpc`](crate::ipc::MpvIpc). -use std::collections::HashMap; +use std::str::FromStr; use serde::{Deserialize, Serialize}; 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 for -/// the upstream list of properties. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum Property { - Path(Option), - Pause(bool), - PlaybackTime(Option), - Duration(Option), - Metadata(Option>), - Unknown { name: String, data: MpvDataType }, +#[serde(rename_all = "kebab-case")] +pub enum EventEndFileReason { + Eof, + Stop, + Quit, + Error, + Redirect, + Unknown, + Unimplemented(String), +} + +impl FromStr for EventEndFileReason { + type Err = (); + + fn from_str(s: &str) -> Result { + 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 { + 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. @@ -36,29 +74,90 @@ pub enum Property { /// See for /// the upstream list of events. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum Event { - Shutdown, - StartFile, - EndFile, + StartFile { + playlist_entry_id: usize, + }, + EndFile { + reason: EventEndFileReason, + playlist_entry_id: usize, + file_error: Option, + playlist_insert_id: Option, + playlist_insert_num_entries: Option, + }, FileLoaded, - TracksChanged, - TrackSwitched, - Idle, - Pause, - Unpause, - Tick, - VideoReconfig, - AudioReconfig, - MetadataUpdate, Seek, 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, + }, + 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, - ClientMessage { args: Vec }, - Unimplemented, + + /// Deprecated since mpv v0.7.0, removed in mpv v0.35.0 + ScriptInputDispatch, + + /// Catch-all for unimplemented events + Unimplemented(Map), } +// 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. +#[allow(deprecated)] pub(crate) fn parse_event(raw_event: MpvIpcEvent) -> Result { let MpvIpcEvent(event) = raw_event; @@ -73,46 +172,129 @@ pub(crate) fn parse_event(raw_event: MpvIpcEvent) -> Result { .ok_or(Error(ErrorCode::ValueDoesNotContainString))?; match event_name { - "shutdown" => Ok(Event::Shutdown), - "start-file" => Ok(Event::StartFile), - "end-file" => Ok(Event::EndFile), + "start-file" => parse_start_file(event), + "end-file" => parse_end_file(event), "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), "playback-restart" => Ok(Event::PlaybackRestart), - "property-change" => parse_event_property(event) - .map(|(id, property)| Event::PropertyChange { id, property }), + "shutdown" => Ok(Event::Shutdown), + "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), - "client-message" => { - 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::, Error>>()?; - Ok(Event::ClientMessage { args }) - } - _ => Ok(Event::Unimplemented), + _ => Ok(Event::Unimplemented(event.to_owned())), } }) } -/// Parse a highlevel [`Property`] object from json, used for [`Event::PropertyChange`]. -fn parse_event_property(event: &Map) -> Result<(usize, Property), Error> { +fn parse_start_file(event: &Map) -> Result { + 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) -> Result { + 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) -> Result { + 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) -> Result { + 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) -> Result { + 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::, Error>>()?; + Ok(Event::ClientMessage { args }) +} + +fn parse_property_change(event: &Map) -> Result { let id = event .get("id") .ok_or(Error(ErrorCode::MissingValue))? @@ -123,38 +305,14 @@ fn parse_event_property(event: &Map) -> Result<(usize, Property), .ok_or(Error(ErrorCode::MissingValue))? .as_str() .ok_or(Error(ErrorCode::ValueDoesNotContainString))?; + let data = event + .get("data") + .ok_or(Error(ErrorCode::MissingValue))? + .clone(); - match property_name { - "path" => { - let path = event - .get("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)), - }, - )) - } - } + Ok(Event::PropertyChange { + id, + name: property_name.to_string(), + data: json_to_value(&data)?, + }) } diff --git a/src/event_property_parser.rs b/src/event_property_parser.rs new file mode 100644 index 0000000..4ace4d2 --- /dev/null +++ b/src/event_property_parser.rs @@ -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 for +/// the upstream list of properties. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Property { + Path(Option), + Pause(bool), + PlaybackTime(Option), + Duration(Option), + Metadata(Option>), + Playlist(Vec), + PlaylistPos(Option), + 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 })), + } +} diff --git a/src/ipc.rs b/src/ipc.rs index 47c0b7b..341c81b 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -51,9 +51,6 @@ impl MpvIpc { } pub(crate) async fn send_command(&mut self, command: &[Value]) -> Result, Error> { - // let lock = self.socket_lock.lock().await; - // START CRITICAL SECTION - let ipc_command = json!({ "command": command }); let ipc_command_str = serde_json::to_string(&ipc_command) .map_err(|why| Error(ErrorCode::JsonParseError(why.to_string())))?; @@ -87,8 +84,6 @@ impl MpvIpc { break parsed_response; } }; - // END CRITICAL SECTION - // mem::drop(lock); log::trace!("Received response: {:?}", response); diff --git a/src/lib.rs b/src/lib.rs index 714ede0..8be302b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ mod core_api; mod error; mod event_parser; +mod event_property_parser; mod highlevel_api_extension; mod ipc; mod message_parser; @@ -10,4 +11,5 @@ mod message_parser; pub use core_api::*; pub use error::*; pub use event_parser::*; +pub use event_property_parser::*; pub use highlevel_api_extension::*; diff --git a/src/message_parser.rs b/src/message_parser.rs index f1e6e80..cef858c 100644 --- a/src/message_parser.rs +++ b/src/message_parser.rs @@ -91,45 +91,36 @@ impl TypeHandler for Vec { } } +pub(crate) fn json_to_value(value: &Value) -> Result { + 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( map: &serde_json::map::Map, ) -> HashMap { let mut output_map: HashMap = HashMap::new(); for (ref key, value) in map.iter() { - match *value { - Value::Array(ref array) => { - output_map.insert( - 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!(); - } + // TODO: proper error handling + if let Ok(value) = json_to_value(value) { + output_map.insert(key.to_string(), value); } } output_map @@ -138,24 +129,8 @@ pub(crate) fn json_map_to_hashmap( pub(crate) fn json_array_to_vec(array: &[Value]) -> Vec { array .iter() - .map(|entry| match entry { - Value::Array(a) => MpvDataType::Array(json_array_to_vec(a)), - 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!(); - } - }) + // TODO: proper error handling + .filter_map(|entry| json_to_value(entry).ok()) .collect() } @@ -198,6 +173,8 @@ mod test { "bool": true, "double": 1.0, "usize": 1, + "minus_one": -1, + "null": null, "string": "string", "object": { "key": "value" @@ -216,6 +193,8 @@ mod test { expected.insert("bool".to_string(), MpvDataType::Bool(true)); expected.insert("double".to_string(), MpvDataType::Double(1.0)); 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( "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); } - #[test] - #[should_panic] - fn test_json_map_to_hashmap_fail_on_null() { - json_map_to_hashmap( - json!({ - "null": null - }) - .as_object() - .unwrap(), - ); - } - #[test] fn test_json_array_to_vec() { let json = json!([ @@ -250,6 +217,8 @@ mod test { true, 1.0, 1, + -1, + null, "string", { "key": "value" @@ -268,6 +237,8 @@ mod test { MpvDataType::Bool(true), MpvDataType::Double(1.0), MpvDataType::Usize(1), + MpvDataType::MinusOne, + MpvDataType::Null, MpvDataType::String("string".to_string()), MpvDataType::HashMap(HashMap::from([( "key".to_string(), @@ -278,12 +249,6 @@ mod test { 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] fn test_json_array_to_playlist() { let json = json!([ diff --git a/tests/events.rs b/tests/events.rs index acf30c1..0d59bef 100644 --- a/tests/events.rs +++ b/tests/events.rs @@ -1,14 +1,12 @@ use std::panic; 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 test_log::test; use tokio::{net::UnixStream, task::JoinHandle}; use tokio_util::codec::{Framed, LinesCodec, LinesCodecError}; -use mpvipc::Event; - fn test_socket( answers: Vec<(bool, String)>, ) -> (UnixStream, JoinHandle>) { @@ -53,19 +51,13 @@ async fn test_observe_event_successful() { tokio::spawn(async move { let event = mpv2.get_event_stream().await.next().await.unwrap().unwrap(); - let property = match event { - Event::PropertyChange { id, property } => { - assert_eq!(id, 1); - property - } - err => panic!("{:?}", err), - }; - let data = match property { - Property::Unknown { name, data } => { + let data = match parse_event_property(event) { + Ok((_, Property::Unknown { name, data })) => { assert_eq!(name, "volume"); data } - err => panic!("{:?}", err), + Ok((_, property)) => panic!("{:?}", property), + Err(err) => panic!("{:?}", err), }; match data { MpvDataType::Double(data) => assert_eq!(data, 64.0), diff --git a/tests/integration.rs b/tests/integration.rs index f967885..87546a6 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -73,11 +73,9 @@ async fn test_events() { let event_checking_thread = tokio::spawn(async move { loop { let event = events.next().await.unwrap().unwrap(); - if let mpvipc::Event::PropertyChange { id, property } = event { - if id == 1337 { - assert_eq!(property, mpvipc::Property::Pause(true)); - break; - } + if let (1337, property) = mpvipc::parse_event_property(event).unwrap() { + assert_eq!(property, mpvipc::Property::Pause(true)); + break; } } }); @@ -86,11 +84,12 @@ async fn test_events() { mpv.set_property("pause", true).await.unwrap(); - if let Err(_) = tokio::time::timeout( + if tokio::time::timeout( tokio::time::Duration::from_millis(500), event_checking_thread, ) .await + .is_err() { panic!("Event checking thread timed out"); }