From 60802e11581f1c2b9c69ef06217ae54e854c8742 Mon Sep 17 00:00:00 2001 From: Oystein Tveit Date: Tue, 30 Apr 2024 10:21:12 +0200 Subject: [PATCH] api: split into several files --- src/{api.rs => core_api.rs} | 378 +++++------------- src/error.rs | 68 ++++ src/event_parser.rs | 141 +++++++ ...xtension.rs => highlevel_api_extension.rs} | 1 + src/lib.rs | 12 +- 5 files changed, 307 insertions(+), 293 deletions(-) rename src/{api.rs => core_api.rs} (64%) create mode 100644 src/error.rs create mode 100644 src/event_parser.rs rename src/{api_extension.rs => highlevel_api_extension.rs} (98%) diff --git a/src/api.rs b/src/core_api.rs similarity index 64% rename from src/api.rs rename to src/core_api.rs index 1542ffa..ea153de 100644 --- a/src/api.rs +++ b/src/core_api.rs @@ -1,50 +1,17 @@ use futures::StreamExt; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::{ - collections::HashMap, - fmt::{self, Display}, -}; +use std::{collections::HashMap, fmt}; use tokio::{ net::UnixStream, sync::{broadcast, mpsc, oneshot}, }; -use crate::ipc::{MpvIpc, MpvIpcCommand, MpvIpcEvent, MpvIpcResponse}; -use crate::message_parser::TypeHandler; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Event { - Shutdown, - StartFile, - EndFile, - FileLoaded, - TracksChanged, - TrackSwitched, - Idle, - Pause, - Unpause, - Tick, - VideoReconfig, - AudioReconfig, - MetadataUpdate, - Seek, - PlaybackRestart, - PropertyChange { id: usize, property: Property }, - ChapterChange, - ClientMessage { args: Vec }, - Unimplemented, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Property { - Path(Option), - Pause(bool), - PlaybackTime(Option), - Duration(Option), - Metadata(Option>), - Unknown { name: String, data: MpvDataType }, -} +use crate::{ + ipc::{MpvIpc, MpvIpcCommand, MpvIpcEvent, MpvIpcResponse}, + message_parser::TypeHandler, + Error, ErrorCode, Event, +}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum MpvCommand { @@ -139,24 +106,6 @@ impl IntoRawCommandPart for SeekOptions { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum ErrorCode { - MpvError(String), - JsonParseError(String), - ConnectError(String), - JsonContainsUnexptectedType, - UnexpectedResult, - UnexpectedValue, - MissingValue, - UnsupportedType, - ValueDoesNotContainBool, - ValueDoesNotContainF64, - ValueDoesNotContainHashMap, - ValueDoesNotContainPlaylist, - ValueDoesNotContainString, - ValueDoesNotContainUsize, -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PlaylistEntry { pub id: usize, @@ -220,52 +169,6 @@ where } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Error(pub ErrorCode); - -impl Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Display::fmt(&self.0, f) - } -} - -impl std::error::Error for Error {} - -impl Display for ErrorCode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - ErrorCode::ConnectError(ref msg) => f.write_str(&format!("ConnectError: {}", msg)), - ErrorCode::JsonParseError(ref msg) => f.write_str(&format!("JsonParseError: {}", msg)), - ErrorCode::MpvError(ref msg) => f.write_str(&format!("MpvError: {}", msg)), - ErrorCode::JsonContainsUnexptectedType => { - f.write_str("Mpv sent a value with an unexpected type") - } - ErrorCode::UnexpectedResult => f.write_str("Unexpected result received"), - ErrorCode::UnexpectedValue => f.write_str("Unexpected value received"), - ErrorCode::MissingValue => f.write_str("Missing value"), - ErrorCode::UnsupportedType => f.write_str("Unsupported type received"), - ErrorCode::ValueDoesNotContainBool => { - f.write_str("The received value is not of type \'std::bool\'") - } - ErrorCode::ValueDoesNotContainF64 => { - f.write_str("The received value is not of type \'std::f64\'") - } - ErrorCode::ValueDoesNotContainHashMap => { - f.write_str("The received value is not of type \'std::collections::HashMap\'") - } - ErrorCode::ValueDoesNotContainPlaylist => { - f.write_str("The received value is not of type \'mpvipc::Playlist\'") - } - ErrorCode::ValueDoesNotContainString => { - f.write_str("The received value is not of type \'std::string::String\'") - } - ErrorCode::ValueDoesNotContainUsize => { - f.write_str("The received value is not of type \'std::usize\'") - } - } - } -} - #[derive(Clone)] pub struct Mpv { command_sender: mpsc::Sender<(MpvIpcCommand, oneshot::Sender)>, @@ -323,7 +226,7 @@ impl Mpv { pub async fn get_event_stream(&self) -> impl futures::Stream> { tokio_stream::wrappers::BroadcastStream::new(self.broadcast_channel.subscribe()).map( |event| match event { - Ok(event) => Mpv::map_event(event), + Ok(event) => crate::event_parser::map_event(event), Err(_) => Err(Error(ErrorCode::ConnectError( "Failed to receive event".to_string(), ))), @@ -331,179 +234,46 @@ impl Mpv { ) } - fn map_event(raw_event: MpvIpcEvent) -> Result { - let MpvIpcEvent(event) = raw_event; - - event - .as_object() - .ok_or(Error(ErrorCode::JsonContainsUnexptectedType)) - .and_then(|event| { - let event_name = event - .get("event") - .ok_or(Error(ErrorCode::MissingValue))? - .as_str() - .ok_or(Error(ErrorCode::ValueDoesNotContainString))?; - - match event_name { - "shutdown" => Ok(Event::Shutdown), - "start-file" => Ok(Event::StartFile), - "end-file" => Ok(Event::EndFile), - "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" => { - let id = event - .get("id") - .ok_or(Error(ErrorCode::MissingValue))? - .as_u64() - .ok_or(Error(ErrorCode::ValueDoesNotContainUsize))? - as usize; - let property_name = event - .get("name") - .ok_or(Error(ErrorCode::MissingValue))? - .as_str() - .ok_or(Error(ErrorCode::ValueDoesNotContainString))?; - - match property_name { - "path" => { - let path = event - .get("data") - .ok_or(Error(ErrorCode::MissingValue))? - .as_str() - .map(|s| s.to_string()); - Ok(Event::PropertyChange { - id, - property: Property::Path(path), - }) - } - "pause" => { - let pause = event - .get("data") - .ok_or(Error(ErrorCode::MissingValue))? - .as_bool() - .ok_or(Error(ErrorCode::ValueDoesNotContainBool))?; - Ok(Event::PropertyChange { - id, - property: Property::Pause(pause), - }) - } - // TODO: missing cases - _ => { - let data = event - .get("data") - .ok_or(Error(ErrorCode::MissingValue))? - .clone(); - Ok(Event::PropertyChange { - id, - property: Property::Unknown { - name: property_name.to_string(), - // TODO: fix - data: MpvDataType::Double(data.as_f64().unwrap_or(0.0)), - }, - }) - } - } - } - "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), - } - }) - } - - /// # Description - /// - /// Retrieves the property value from mpv. - /// - /// ## Supported types - /// - String - /// - bool - /// - HashMap (e.g. for the 'metadata' property) - /// - Vec (for the 'playlist' property) - /// - usize - /// - f64 - /// - /// ## Input arguments - /// - /// - **property** defines the mpv property that should be retrieved - /// - /// # Example - /// ``` - /// use mpvipc::{Mpv, Error}; - /// async fn main() -> Result<(), Error> { - /// let mpv = Mpv::connect("/tmp/mpvsocket")?; - /// let paused: bool = mpv.get_property("pause").await?; - /// let title: String = mpv.get_property("media-title").await?; - /// Ok(()) - /// } - /// ``` - pub async fn get_property( + /// Run a custom command. + /// This should only be used if the desired command is not implemented + /// with [MpvCommand]. + pub async fn run_command_raw( &self, - property: &str, - ) -> Result { - T::get_property_generic(self, property).await - } - - /// # Description - /// - /// Retrieves the property value from mpv. - /// The result is always of type String, regardless of the type of the value of the mpv property - /// - /// ## Input arguments - /// - /// - **property** defines the mpv property that should be retrieved - /// - /// # Example - /// - /// ``` - /// use mpvipc::{Mpv, Error}; - /// fn main() -> Result<(), Error> { - /// let mpv = Mpv::connect("/tmp/mpvsocket")?; - /// let title = mpv.get_property_string("media-title")?; - /// Ok(()) - /// } - /// ``` - pub async fn get_property_value(&self, property: &str) -> Result { + command: &str, + args: &[&str], + ) -> Result, Error> { + let command = Vec::from( + [command] + .iter() + .chain(args.iter()) + .map(|s| s.to_string()) + .collect::>() + .as_slice(), + ); let (res_tx, res_rx) = oneshot::channel(); self.command_sender - .send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx)) + .send((MpvIpcCommand::Command(command), res_tx)) .await .map_err(|_| { Error(ErrorCode::ConnectError( "Failed to send command".to_string(), )) })?; + match res_rx.await { - Ok(MpvIpcResponse(response)) => { - response.and_then(|value| value.ok_or(Error(ErrorCode::MissingValue))) - } + Ok(MpvIpcResponse(response)) => response, Err(err) => Err(Error(ErrorCode::ConnectError(err.to_string()))), } } + async fn run_command_raw_ignore_value( + &self, + command: &str, + args: &[&str], + ) -> Result<(), Error> { + self.run_command_raw(command, args).await.map(|_| ()) + } + /// # Description /// /// Runs mpv commands. The arguments are passed as a String-Vector reference: @@ -630,46 +400,76 @@ impl Mpv { result } - /// Run a custom command. - /// This should only be used if the desired command is not implemented - /// with [MpvCommand]. - pub async fn run_command_raw( + /// # Description + /// + /// Retrieves the property value from mpv. + /// + /// ## Supported types + /// - String + /// - bool + /// - HashMap (e.g. for the 'metadata' property) + /// - Vec (for the 'playlist' property) + /// - usize + /// - f64 + /// + /// ## Input arguments + /// + /// - **property** defines the mpv property that should be retrieved + /// + /// # Example + /// ``` + /// use mpvipc::{Mpv, Error}; + /// async fn main() -> Result<(), Error> { + /// let mpv = Mpv::connect("/tmp/mpvsocket")?; + /// let paused: bool = mpv.get_property("pause").await?; + /// let title: String = mpv.get_property("media-title").await?; + /// Ok(()) + /// } + /// ``` + pub async fn get_property( &self, - command: &str, - args: &[&str], - ) -> Result, Error> { - let command = Vec::from( - [command] - .iter() - .chain(args.iter()) - .map(|s| s.to_string()) - .collect::>() - .as_slice(), - ); + property: &str, + ) -> Result { + T::get_property_generic(self, property).await + } + + /// # Description + /// + /// Retrieves the property value from mpv. + /// The result is always of type String, regardless of the type of the value of the mpv property + /// + /// ## Input arguments + /// + /// - **property** defines the mpv property that should be retrieved + /// + /// # Example + /// + /// ``` + /// use mpvipc::{Mpv, Error}; + /// fn main() -> Result<(), Error> { + /// let mpv = Mpv::connect("/tmp/mpvsocket")?; + /// let title = mpv.get_property_string("media-title")?; + /// Ok(()) + /// } + /// ``` + pub async fn get_property_value(&self, property: &str) -> Result { let (res_tx, res_rx) = oneshot::channel(); self.command_sender - .send((MpvIpcCommand::Command(command), res_tx)) + .send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx)) .await .map_err(|_| { Error(ErrorCode::ConnectError( "Failed to send command".to_string(), )) })?; - match res_rx.await { - Ok(MpvIpcResponse(response)) => response, + Ok(MpvIpcResponse(response)) => { + response.and_then(|value| value.ok_or(Error(ErrorCode::MissingValue))) + } Err(err) => Err(Error(ErrorCode::ConnectError(err.to_string()))), } } - async fn run_command_raw_ignore_value( - &self, - command: &str, - args: &[&str], - ) -> Result<(), Error> { - self.run_command_raw(command, args).await.map(|_| ()) - } - /// # Description /// /// Sets the mpv property __ to __. diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..b6b7c03 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,68 @@ +use core::fmt; +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ErrorCode { + MpvError(String), + JsonParseError(String), + ConnectError(String), + JsonContainsUnexptectedType, + UnexpectedResult, + UnexpectedValue, + MissingValue, + UnsupportedType, + ValueDoesNotContainBool, + ValueDoesNotContainF64, + ValueDoesNotContainHashMap, + ValueDoesNotContainPlaylist, + ValueDoesNotContainString, + ValueDoesNotContainUsize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Error(pub ErrorCode); + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl std::error::Error for Error {} + +impl Display for ErrorCode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ErrorCode::ConnectError(ref msg) => f.write_str(&format!("ConnectError: {}", msg)), + ErrorCode::JsonParseError(ref msg) => f.write_str(&format!("JsonParseError: {}", msg)), + ErrorCode::MpvError(ref msg) => f.write_str(&format!("MpvError: {}", msg)), + ErrorCode::JsonContainsUnexptectedType => { + f.write_str("Mpv sent a value with an unexpected type") + } + ErrorCode::UnexpectedResult => f.write_str("Unexpected result received"), + ErrorCode::UnexpectedValue => f.write_str("Unexpected value received"), + ErrorCode::MissingValue => f.write_str("Missing value"), + ErrorCode::UnsupportedType => f.write_str("Unsupported type received"), + ErrorCode::ValueDoesNotContainBool => { + f.write_str("The received value is not of type \'std::bool\'") + } + ErrorCode::ValueDoesNotContainF64 => { + f.write_str("The received value is not of type \'std::f64\'") + } + ErrorCode::ValueDoesNotContainHashMap => { + f.write_str("The received value is not of type \'std::collections::HashMap\'") + } + ErrorCode::ValueDoesNotContainPlaylist => { + f.write_str("The received value is not of type \'mpvipc::Playlist\'") + } + ErrorCode::ValueDoesNotContainString => { + f.write_str("The received value is not of type \'std::string::String\'") + } + ErrorCode::ValueDoesNotContainUsize => { + f.write_str("The received value is not of type \'std::usize\'") + } + } + } +} \ No newline at end of file diff --git a/src/event_parser.rs b/src/event_parser.rs new file mode 100644 index 0000000..b02c9cf --- /dev/null +++ b/src/event_parser.rs @@ -0,0 +1,141 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::{ipc::MpvIpcEvent, Error, ErrorCode, MpvDataType}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Property { + Path(Option), + Pause(bool), + PlaybackTime(Option), + Duration(Option), + Metadata(Option>), + Unknown { name: String, data: MpvDataType }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Event { + Shutdown, + StartFile, + EndFile, + FileLoaded, + TracksChanged, + TrackSwitched, + Idle, + Pause, + Unpause, + Tick, + VideoReconfig, + AudioReconfig, + MetadataUpdate, + Seek, + PlaybackRestart, + PropertyChange { id: usize, property: Property }, + ChapterChange, + ClientMessage { args: Vec }, + Unimplemented, +} + +pub(crate) fn map_event(raw_event: MpvIpcEvent) -> Result { + let MpvIpcEvent(event) = raw_event; + + event + .as_object() + .ok_or(Error(ErrorCode::JsonContainsUnexptectedType)) + .and_then(|event| { + let event_name = event + .get("event") + .ok_or(Error(ErrorCode::MissingValue))? + .as_str() + .ok_or(Error(ErrorCode::ValueDoesNotContainString))?; + + match event_name { + "shutdown" => Ok(Event::Shutdown), + "start-file" => Ok(Event::StartFile), + "end-file" => Ok(Event::EndFile), + "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" => { + let id = event + .get("id") + .ok_or(Error(ErrorCode::MissingValue))? + .as_u64() + .ok_or(Error(ErrorCode::ValueDoesNotContainUsize))? + as usize; + let property_name = event + .get("name") + .ok_or(Error(ErrorCode::MissingValue))? + .as_str() + .ok_or(Error(ErrorCode::ValueDoesNotContainString))?; + + match property_name { + "path" => { + let path = event + .get("data") + .ok_or(Error(ErrorCode::MissingValue))? + .as_str() + .map(|s| s.to_string()); + Ok(Event::PropertyChange { + id, + property: Property::Path(path), + }) + } + "pause" => { + let pause = event + .get("data") + .ok_or(Error(ErrorCode::MissingValue))? + .as_bool() + .ok_or(Error(ErrorCode::ValueDoesNotContainBool))?; + Ok(Event::PropertyChange { + id, + property: Property::Pause(pause), + }) + } + // TODO: missing cases + _ => { + let data = event + .get("data") + .ok_or(Error(ErrorCode::MissingValue))? + .clone(); + Ok(Event::PropertyChange { + id, + property: Property::Unknown { + name: property_name.to_string(), + // TODO: fix + data: MpvDataType::Double(data.as_f64().unwrap_or(0.0)), + }, + }) + } + } + } + "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), + } + }) +} diff --git a/src/api_extension.rs b/src/highlevel_api_extension.rs similarity index 98% rename from src/api_extension.rs rename to src/highlevel_api_extension.rs index f95d653..1810d56 100644 --- a/src/api_extension.rs +++ b/src/highlevel_api_extension.rs @@ -29,6 +29,7 @@ pub enum Switch { Toggle, } +/// The `MpvExt` trait provides a set of typesafe high-level functions to interact with mpv. // TODO: fix this #[allow(async_fn_in_trait)] pub trait MpvExt { diff --git a/src/lib.rs b/src/lib.rs index aca7f54..20b214e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,11 @@ -mod api; -mod api_extension; +mod highlevel_api_extension; +mod core_api; +mod error; +mod event_parser; mod ipc; mod message_parser; -pub use api::*; -pub use api_extension::*; +pub use highlevel_api_extension::*; +pub use core_api::*; +pub use error::*; +pub use event_parser::*;