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
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:
parent
f5ca2ebde9
commit
7eec34ce00
|
@ -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),
|
||||||
|
|
|
@ -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)),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })),
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -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!([
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue