Compare commits

..

No commits in common. "bfc27531ef1e7574297e98da9f038341715cf6f1" and "3be7b2bda68d1677eb41301cb429e1947f010f49" have entirely different histories.

11 changed files with 382 additions and 718 deletions

View File

@ -22,7 +22,7 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Build - name: Build
run: cargo build --all-features --verbose --release run: cargo build --all-features --verbose
check: check:
runs-on: ubuntu-latest-personal runs-on: ubuntu-latest-personal
@ -49,62 +49,16 @@ jobs:
runs-on: ubuntu-latest-personal runs-on: ubuntu-latest-personal
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: cargo-bins/cargo-binstall@main
- name: Install latest nightly toolchain - name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: nightly toolchain: nightly
override: true override: true
components: rustfmt, clippy, llvm-tools-preview components: rustfmt, clippy
- name: Cache dependencies - name: Cache dependencies
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Create necessary directories
run: mkdir -p target/test-report
- name: Run tests - name: Run tests
run: cargo test --all-features --release -Z unstable-options --report-time --format json | tee target/test-report/test-report.json run: cargo test --all-features --verbose
env:
RUSTFLAGS: "-Cinstrument-coverage"
LLVM_PROFILE_FILE: "target/release/coverage/%p-%m.profraw"
- name: Install markdown-test-report
run: cargo binstall -y markdown-test-report
- name: Generate test report
run: markdown-test-report target/test-report/test-report.json target/test-report/test-report.md
- name: Upload test report
uses: actions/upload-artifact@v4
with:
name: test-report
path: target/test-report/test-report.md
- name: Install grcov
run: cargo binstall -y grcov
- name: Generate coverage report
run: |
grcov \
--source-dir . \
--binary-path ./target/release/deps/ \
--excl-start 'mod test* \{' \
--ignore 'tests/*' \
--ignore "*test.rs" \
--ignore "*tests.rs" \
--ignore "*github.com*" \
--ignore "*libcore*" \
--ignore "*rustc*" \
--ignore "*liballoc*" \
--ignore "*cargo*" \
-t html \
-o ./target/coverage/html \
target/coverage/prof
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage
path: target/coverage/html

View File

@ -22,8 +22,7 @@ tokio-stream = { version = "0.1.15", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
env_logger = "0.10.0" env_logger = "0.10.0"
test-log = "0.2.15" test-log = "0.2.15"
tokio = { version = "1.37.0", features = ["rt-multi-thread", "time", "process"] } tokio = { version = "1.37.0", features = ["rt-multi-thread", "time"] }
uuid = { version = "1.8.0", features = ["v4"] }
[lib] [lib]
doctest = false doctest = false

44
run.sh
View File

@ -1,44 +0,0 @@
#!/usr/bin/env bash
rm -rf target
mkdir -p target/test-report
export RUSTFLAGS="-Cinstrument-coverage"
export LLVM_PROFILE_FILE="target/coverage/prof/%p-%m.profraw"
rustup override set nightly
echo "Running tests..."
cargo test --all-features --release --no-fail-fast -- -Z unstable-options --report-time --format json | tee target/test-report/test-report.json
echo "Generating test report..."
markdown-test-report target/test-report/test-report.json --output target/test-report/test-report.md
echo "Generating test report HTML..."
pandoc target/test-report/test-report.md -o target/test-report/test-report.html
# rustup override set stable
echo "Removing unused profraw files..."
for file in target/coverage/prof/*.profraw; do
~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata show "$file" 1>/dev/null 2>/dev/null || rm -f "$file"
done
echo "Generating coverage report..."
grcov \
--source-dir . \
--binary-path ./target/release/deps/ \
--excl-start 'mod test* \{' \
--ignore 'tests/*' \
--ignore "*test.rs" \
--ignore "*tests.rs" \
--ignore "*github.com*" \
--ignore "*libcore*" \
--ignore "*rustc*" \
--ignore "*liballoc*" \
--ignore "*cargo*" \
-t html \
-o ./target/coverage/html \
target/coverage/prof
rustup override set nightly

View File

@ -1,30 +1,51 @@
//! The core API for interacting with [`Mpv`].
use futures::StreamExt; use futures::StreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::{collections::HashMap, fmt}; use std::{
collections::HashMap,
fmt::{self, Display},
};
use tokio::{ use tokio::{
net::UnixStream, net::UnixStream,
sync::{broadcast, mpsc, oneshot}, sync::{broadcast, mpsc, oneshot},
}; };
use crate::{ use crate::ipc::{MpvIpc, MpvIpcCommand, MpvIpcEvent, MpvIpcResponse};
ipc::{MpvIpc, MpvIpcCommand, MpvIpcEvent, MpvIpcResponse}, use crate::message_parser::TypeHandler;
message_parser::TypeHandler,
Error, ErrorCode, Event, #[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<String> },
Unimplemented,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Property {
Path(Option<String>),
Pause(bool),
PlaybackTime(Option<f64>),
Duration(Option<f64>),
Metadata(Option<HashMap<String, MpvDataType>>),
Unknown { name: String, data: MpvDataType },
}
/// All possible commands that can be sent to mpv.
///
/// Not all commands are guaranteed to be implemented.
/// If something is missing, please open an issue.
///
/// You can also use the `run_command_raw` function to run commands
/// that are not implemented here.
///
/// See <https://mpv.io/manual/master/#list-of-input-commands> for
/// the upstream list of commands.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MpvCommand { pub enum MpvCommand {
LoadFile { LoadFile {
@ -62,13 +83,11 @@ pub enum MpvCommand {
Unobserve(isize), Unobserve(isize),
} }
/// Helper trait to keep track of the string literals that mpv expects.
pub(crate) trait IntoRawCommandPart { pub(crate) trait IntoRawCommandPart {
fn into_raw_command_part(self) -> String; fn into_raw_command_part(self) -> String;
} }
/// Generic data type representing all possible data types that mpv can return. #[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum MpvDataType { pub enum MpvDataType {
Array(Vec<MpvDataType>), Array(Vec<MpvDataType>),
Bool(bool), Bool(bool),
@ -80,20 +99,6 @@ pub enum MpvDataType {
Usize(usize), Usize(usize),
} }
/// A mpv playlist.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Playlist(pub Vec<PlaylistEntry>);
/// A single entry in the mpv playlist.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlaylistEntry {
pub id: usize,
pub filename: String,
pub title: String,
pub current: bool,
}
/// Options for [`MpvCommand::LoadFile`] and [`MpvCommand::LoadList`].
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum PlaylistAddOptions { pub enum PlaylistAddOptions {
Replace, Replace,
@ -109,7 +114,12 @@ impl IntoRawCommandPart for PlaylistAddOptions {
} }
} }
/// Options for [`MpvCommand::Seek`]. #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum PlaylistAddTypeOptions {
File,
Playlist,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum SeekOptions { pub enum SeekOptions {
Relative, Relative,
@ -129,7 +139,35 @@ impl IntoRawCommandPart for SeekOptions {
} }
} }
/// A trait for specifying how to extract and parse a value returned through [`Mpv::get_property`]. #[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,
pub filename: String,
pub title: String,
pub current: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Playlist(pub Vec<PlaylistEntry>);
pub trait GetPropertyTypeHandler: Sized { pub trait GetPropertyTypeHandler: Sized {
// TODO: fix this // TODO: fix this
#[allow(async_fn_in_trait)] #[allow(async_fn_in_trait)]
@ -148,7 +186,6 @@ where
} }
} }
/// A trait for specifying how to serialize and set a value through [`Mpv::set_property`].
pub trait SetPropertyTypeHandler<T> { pub trait SetPropertyTypeHandler<T> {
// TODO: fix this // TODO: fix this
#[allow(async_fn_in_trait)] #[allow(async_fn_in_trait)]
@ -183,14 +220,52 @@ where
} }
} }
/// The main struct for interacting with mpv. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// pub struct Error(pub ErrorCode);
/// This struct provides the core API for interacting with mpv.
/// These functions are the building blocks for the higher-level API provided by the `MpvExt` trait. impl Display for Error {
/// They can also be used directly to interact with mpv in a more flexible way, mostly returning JSON values. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
/// Display::fmt(&self.0, f)
/// The `Mpv` struct can be cloned freely, and shared anywhere. }
/// It only contains a message passing channel to the tokio task that handles the IPC communication with mpv. }
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)] #[derive(Clone)]
pub struct Mpv { pub struct Mpv {
command_sender: mpsc::Sender<(MpvIpcCommand, oneshot::Sender<MpvIpcResponse>)>, command_sender: mpsc::Sender<(MpvIpcCommand, oneshot::Sender<MpvIpcResponse>)>,
@ -248,7 +323,7 @@ impl Mpv {
pub async fn get_event_stream(&self) -> impl futures::Stream<Item = Result<Event, Error>> { pub async fn get_event_stream(&self) -> impl futures::Stream<Item = Result<Event, Error>> {
tokio_stream::wrappers::BroadcastStream::new(self.broadcast_channel.subscribe()).map( tokio_stream::wrappers::BroadcastStream::new(self.broadcast_channel.subscribe()).map(
|event| match event { |event| match event {
Ok(event) => crate::event_parser::parse_event(event), Ok(event) => Mpv::map_event(event),
Err(_) => Err(Error(ErrorCode::ConnectError( Err(_) => Err(Error(ErrorCode::ConnectError(
"Failed to receive event".to_string(), "Failed to receive event".to_string(),
))), ))),
@ -256,46 +331,179 @@ impl Mpv {
) )
} }
/// Run a custom command. fn map_event(raw_event: MpvIpcEvent) -> Result<Event, Error> {
/// This should only be used if the desired command is not implemented let MpvIpcEvent(event) = raw_event;
/// with [MpvCommand].
pub async fn run_command_raw( event
&self, .as_object()
command: &str, .ok_or(Error(ErrorCode::JsonContainsUnexptectedType))
args: &[&str], .and_then(|event| {
) -> Result<Option<Value>, Error> { let event_name = event
let command = Vec::from( .get("event")
[command] .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() .iter()
.chain(args.iter()) .map(|arg| {
arg.as_str()
.ok_or(Error(ErrorCode::ValueDoesNotContainString))
.map(|s| s.to_string()) .map(|s| s.to_string())
.collect::<Vec<String>>() })
.as_slice(), .collect::<Result<Vec<String>, Error>>()?;
); Ok(Event::ClientMessage { args })
}
_ => Ok(Event::Unimplemented),
}
})
}
/// # Description
///
/// Retrieves the property value from mpv.
///
/// ## Supported types
/// - String
/// - bool
/// - HashMap<String, String> (e.g. for the 'metadata' property)
/// - Vec<PlaylistEntry> (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<T: GetPropertyTypeHandler>(
&self,
property: &str,
) -> Result<T, Error> {
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<Value, Error> {
let (res_tx, res_rx) = oneshot::channel(); let (res_tx, res_rx) = oneshot::channel();
self.command_sender self.command_sender
.send((MpvIpcCommand::Command(command), res_tx)) .send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx))
.await .await
.map_err(|_| { .map_err(|_| {
Error(ErrorCode::ConnectError( Error(ErrorCode::ConnectError(
"Failed to send command".to_string(), "Failed to send command".to_string(),
)) ))
})?; })?;
match res_rx.await { 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()))), 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 /// # Description
/// ///
/// Runs mpv commands. The arguments are passed as a String-Vector reference: /// Runs mpv commands. The arguments are passed as a String-Vector reference:
@ -303,7 +511,7 @@ impl Mpv {
/// ## Input arguments /// ## Input arguments
/// ///
/// - **command** defines the mpv command that should be executed /// - **command** defines the mpv command that should be executed
/// - **args** a slice of `&str`'s which define the arguments /// - **args** a slice of &str's which define the arguments
/// ///
/// # Example /// # Example
/// ``` /// ```
@ -422,90 +630,60 @@ impl Mpv {
result result
} }
/// # Description /// Run a custom command.
/// /// This should only be used if the desired command is not implemented
/// Retrieves the property value from mpv. /// with [MpvCommand].
/// pub async fn run_command_raw(
/// ## Supported types
/// - `String`
/// - `bool`
/// - `HashMap<String, String>` (e.g. for the 'metadata' property)
/// - `Vec<PlaylistEntry>` (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<T: GetPropertyTypeHandler>(
&self, &self,
property: &str, command: &str,
) -> Result<T, Error> { args: &[&str],
T::get_property_generic(self, property).await ) -> Result<Option<Value>, Error> {
} let command = Vec::from(
[command]
/// # Description .iter()
/// .chain(args.iter())
/// Retrieves the property value from mpv. .map(|s| s.to_string())
/// The result is always of type String, regardless of the type of the value of the mpv property .collect::<Vec<String>>()
/// .as_slice(),
/// ## 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<Value, Error> {
let (res_tx, res_rx) = oneshot::channel(); let (res_tx, res_rx) = oneshot::channel();
self.command_sender self.command_sender
.send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx)) .send((MpvIpcCommand::Command(command), res_tx))
.await .await
.map_err(|_| { .map_err(|_| {
Error(ErrorCode::ConnectError( Error(ErrorCode::ConnectError(
"Failed to send command".to_string(), "Failed to send command".to_string(),
)) ))
})?; })?;
match res_rx.await { match res_rx.await {
Ok(MpvIpcResponse(response)) => { Ok(MpvIpcResponse(response)) => response,
response.and_then(|value| value.ok_or(Error(ErrorCode::MissingValue)))
}
Err(err) => Err(Error(ErrorCode::ConnectError(err.to_string()))), 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 /// # Description
/// ///
/// Sets the mpv property _`<property>`_ to _`<value>`_. /// Sets the mpv property _<property>_ to _<value>_.
/// ///
/// ## Supported types /// ## Supported types
/// - `String` /// - String
/// - `bool` /// - bool
/// - `f64` /// - f64
/// - `usize` /// - usize
/// ///
/// ## Input arguments /// ## Input arguments
/// ///
/// - **property** defines the mpv property that should be retrieved /// - **property** defines the mpv property that should be retrieved
/// - **value** defines the value of the given mpv property _`<property>`_ /// - **value** defines the value of the given mpv property _<property>_
/// ///
/// # Example /// # Example
/// ``` /// ```

View File

@ -1,13 +1,10 @@
//! High-level API extension for [`Mpv`].
use crate::{ use crate::{
Error, IntoRawCommandPart, Mpv, MpvCommand, MpvDataType, Playlist, PlaylistAddOptions, Error, IntoRawCommandPart, Mpv, MpvCommand, MpvDataType, Playlist, PlaylistAddOptions,
PlaylistEntry, SeekOptions, PlaylistAddTypeOptions, PlaylistEntry, SeekOptions,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
/// Generic high-level command for changing a number property.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NumberChangeOptions { pub enum NumberChangeOptions {
Absolute, Absolute,
@ -25,7 +22,6 @@ impl IntoRawCommandPart for NumberChangeOptions {
} }
} }
/// Generic high-level switch for toggling boolean properties.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Switch { pub enum Switch {
On, On,
@ -33,14 +29,6 @@ pub enum Switch {
Toggle, Toggle,
} }
/// Options for [`MpvExt::playlist_add`].
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum PlaylistAddTypeOptions {
File,
Playlist,
}
/// A set of typesafe high-level functions to interact with [`Mpv`].
// TODO: fix this // TODO: fix this
#[allow(async_fn_in_trait)] #[allow(async_fn_in_trait)]
pub trait MpvExt { pub trait MpvExt {

View File

@ -1,72 +0,0 @@
//! Library specific error messages.
use core::fmt;
use std::fmt::Display;
use serde::{Deserialize, Serialize};
/// All possible errors that can occur when interacting with mpv.
#[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,
}
/// Any error that can occur when interacting with mpv.
#[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\'")
}
}
}
}

View File

@ -1,160 +0,0 @@
//! JSON parsing logic for events from [`MpvIpc`](crate::ipc::MpvIpc).
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::{ipc::MpvIpcEvent, 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, Serialize, Deserialize)]
pub enum Property {
Path(Option<String>),
Pause(bool),
PlaybackTime(Option<f64>),
Duration(Option<f64>),
Metadata(Option<HashMap<String, MpvDataType>>),
Unknown { name: String, data: MpvDataType },
}
/// All possible events that can be sent by mpv.
///
/// Not all event types are guaranteed to be implemented.
/// If something is missing, please open an issue.
///
/// Otherwise, the event will be returned as an `Event::Unimplemented` variant.
///
/// See <https://mpv.io/manual/master/#list-of-events> for
/// the upstream list of events.
#[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<String> },
Unimplemented,
}
/// Parse a highlevel [`Event`] objects from json.
pub(crate) fn parse_event(raw_event: MpvIpcEvent) -> Result<Event, Error> {
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" => parse_event_property(event)
.and_then(|(id, property)| Ok(Event::PropertyChange { id, property })),
"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::<Result<Vec<String>, Error>>()?;
Ok(Event::ClientMessage { args })
}
_ => Ok(Event::Unimplemented),
}
})
}
/// Parse a highlevel [`Property`] object from json, used for [`Event::PropertyChange`].
fn parse_event_property(event: &Map<String, Value>) -> Result<(usize, Property), Error> {
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((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

@ -1,28 +1,19 @@
//! IPC handling thread/task. Handles communication between [`Mpv`](crate::Mpv) instances and mpv's unix socket use super::*;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::mem; use std::mem;
use tokio::{ use tokio::net::UnixStream;
net::UnixStream, use tokio::sync::mpsc;
sync::{broadcast, mpsc, oneshot, Mutex}, use tokio::sync::{broadcast, oneshot, Mutex};
};
use tokio_util::codec::{Framed, LinesCodec, LinesCodecError}; use tokio_util::codec::{Framed, LinesCodec, LinesCodecError};
use crate::{Error, ErrorCode};
/// Container for all state that regards communication with the mpv IPC socket
/// and message passing with [`Mpv`](crate::Mpv) controllers.
pub(crate) struct MpvIpc { pub(crate) struct MpvIpc {
socket: Framed<UnixStream, LinesCodec>, socket: Framed<UnixStream, LinesCodec>,
// I had trouble with reading and writing to the socket when it was wrapped
// in a MutexGuard, so I'm using a separate Mutex to lock the socket when needed.
socket_lock: Mutex<()>,
command_channel: mpsc::Receiver<(MpvIpcCommand, oneshot::Sender<MpvIpcResponse>)>, command_channel: mpsc::Receiver<(MpvIpcCommand, oneshot::Sender<MpvIpcResponse>)>,
socket_lock: Mutex<()>,
event_channel: broadcast::Sender<MpvIpcEvent>, event_channel: broadcast::Sender<MpvIpcEvent>,
} }
/// Commands that can be sent to [`MpvIpc`]
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum MpvIpcCommand { pub(crate) enum MpvIpcCommand {
Command(Vec<String>), Command(Vec<String>),
@ -33,11 +24,9 @@ pub(crate) enum MpvIpcCommand {
Exit, Exit,
} }
/// [`MpvIpc`]'s response to a [`MpvIpcCommand`].
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct MpvIpcResponse(pub(crate) Result<Option<Value>, Error>); pub(crate) struct MpvIpcResponse(pub(crate) Result<Option<Value>, Error>);
/// A deserialized and partially parsed event from mpv.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct MpvIpcEvent(pub(crate) Value); pub(crate) struct MpvIpcEvent(pub(crate) Value);
@ -99,6 +88,10 @@ impl MpvIpc {
property: &str, property: &str,
value: Value, value: Value,
) -> Result<Option<Value>, Error> { ) -> Result<Option<Value>, Error> {
// let str_value = match &value {
// Value::String(s) => s,
// v => &serde_json::to_string(&v).unwrap(),
// };
self.send_command(&[json!("set_property"), json!(property), value]) self.send_command(&[json!("set_property"), json!(property), value])
.await .await
} }
@ -184,9 +177,6 @@ impl MpvIpc {
} }
} }
/// This function does the most basic JSON parsing and error handling
/// for status codes and errors that all responses from mpv are
/// expected to contain.
fn parse_mpv_response_data(value: Value) -> Result<Option<Value>, Error> { fn parse_mpv_response_data(value: Value) -> Result<Option<Value>, Error> {
log::trace!("Parsing mpv response data: {:?}", value); log::trace!("Parsing mpv response data: {:?}", value);
let result = value let result = value

View File

@ -1,13 +1,7 @@
#![doc = include_str!("../README.md")] mod api;
mod api_extension;
mod core_api;
mod error;
mod event_parser;
mod highlevel_api_extension;
mod ipc; mod ipc;
mod message_parser; mod message_parser;
pub use core_api::*; pub use api::*;
pub use error::*; pub use api_extension::*;
pub use event_parser::*;
pub use highlevel_api_extension::*;

View File

@ -1,5 +1,3 @@
//! JSON parsing logic for command responses from [`MpvIpc`](crate::ipc::MpvIpc).
use std::collections::HashMap; use std::collections::HashMap;
use serde_json::Value; use serde_json::Value;
@ -136,27 +134,61 @@ 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 let mut output: Vec<MpvDataType> = Vec::new();
.into_iter() if !array.is_empty() {
.map(|entry| match entry { match array[0] {
Value::Array(a) => MpvDataType::Array(json_array_to_vec(&a)), Value::Array(_) => {
Value::Bool(b) => MpvDataType::Bool(*b), for entry in array {
Value::Number(n) => { if let Value::Array(ref a) = *entry {
output.push(MpvDataType::Array(json_array_to_vec(a)));
}
}
}
Value::Bool(_) => {
for entry in array {
if let Value::Bool(ref b) = *entry {
output.push(MpvDataType::Bool(*b));
}
}
}
Value::Number(_) => {
for entry in array {
if let Value::Number(ref n) = *entry {
if n.is_u64() { if n.is_u64() {
MpvDataType::Usize(n.as_u64().unwrap() as usize) output.push(MpvDataType::Usize(n.as_u64().unwrap() as usize));
} else if n.is_f64() { } else if n.is_f64() {
MpvDataType::Double(n.as_f64().unwrap()) output.push(MpvDataType::Double(n.as_f64().unwrap()));
} else { } else {
panic!("unimplemented number"); panic!("unimplemented number");
} }
} }
Value::Object(o) => MpvDataType::HashMap(json_map_to_hashmap(&o)), }
Value::String(s) => MpvDataType::String(s.to_owned()), }
Value::Object(_) => {
for entry in array {
if let Value::Object(ref map) = *entry {
output.push(MpvDataType::HashMap(json_map_to_hashmap(map)));
}
}
}
Value::String(_) => {
for entry in array {
if let Value::String(ref s) = *entry {
output.push(MpvDataType::String(s.to_string()));
}
}
}
Value::Null => { Value::Null => {
unimplemented!(); unimplemented!();
} }
}) }
.collect() }
output
} }
pub(crate) fn json_array_to_playlist(array: &[Value]) -> Vec<PlaylistEntry> { pub(crate) fn json_array_to_playlist(array: &[Value]) -> Vec<PlaylistEntry> {
@ -183,137 +215,3 @@ pub(crate) fn json_array_to_playlist(array: &[Value]) -> Vec<PlaylistEntry> {
} }
output output
} }
#[cfg(test)]
mod test {
use super::*;
use crate::MpvDataType;
use serde_json::json;
use std::collections::HashMap;
#[test]
fn test_json_map_to_hashmap() {
let json = json!({
"array": [1, 2, 3],
"bool": true,
"double": 1.0,
"usize": 1,
"string": "string",
"object": {
"key": "value"
}
});
let mut expected = HashMap::new();
expected.insert(
"array".to_string(),
MpvDataType::Array(vec![
MpvDataType::Usize(1),
MpvDataType::Usize(2),
MpvDataType::Usize(3),
]),
);
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(
"string".to_string(),
MpvDataType::String("string".to_string()),
);
expected.insert(
"object".to_string(),
MpvDataType::HashMap(HashMap::from([(
"key".to_string(),
MpvDataType::String("value".to_string()),
)])),
);
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!([
[1, 2, 3],
true,
1.0,
1,
"string",
{
"key": "value"
}
]);
println!("{:?}", json.as_array().unwrap());
println!("{:?}", json_array_to_vec(&json.as_array().unwrap()));
let expected = vec![
MpvDataType::Array(vec![
MpvDataType::Usize(1),
MpvDataType::Usize(2),
MpvDataType::Usize(3),
]),
MpvDataType::Bool(true),
MpvDataType::Double(1.0),
MpvDataType::Usize(1),
MpvDataType::String("string".to_string()),
MpvDataType::HashMap(HashMap::from([(
"key".to_string(),
MpvDataType::String("value".to_string()),
)])),
];
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!([
{
"filename": "file1",
"title": "title1",
"current": true
},
{
"filename": "file2",
"title": "title2",
"current": false
}
]);
let expected = vec![
PlaylistEntry {
id: 0,
filename: "file1".to_string(),
title: "title1".to_string(),
current: true,
},
PlaylistEntry {
id: 1,
filename: "file2".to_string(),
title: "title2".to_string(),
current: false,
},
];
assert_eq!(json_array_to_playlist(&json.as_array().unwrap()), expected);
}
}

View File

@ -1,61 +0,0 @@
use mpvipc::{Error, Mpv, MpvExt};
use std::path::Path;
use tokio::{
process::{Child, Command},
time::{sleep, timeout, Duration},
};
#[cfg(target_family = "unix")]
async fn spawn_headless_mpv() -> Result<(Child, Mpv), Error> {
let socket_path_str = format!("/tmp/mpv-ipc-{}", uuid::Uuid::new_v4());
let socket_path = Path::new(&socket_path_str);
let process_handle = Command::new("mpv")
.arg("--no-config")
.arg("--idle")
.arg("--no-video")
.arg("--no-audio")
.arg(format!(
"--input-ipc-server={}",
&socket_path.to_str().unwrap()
))
.spawn()
.expect("Failed to start mpv");
if timeout(Duration::from_millis(500), async {
while !&socket_path.exists() {
sleep(Duration::from_millis(10)).await;
}
})
.await
.is_err()
{
panic!("Failed to create mpv socket at {:?}", &socket_path);
}
let mpv = Mpv::connect(socket_path.to_str().unwrap()).await.unwrap();
Ok((process_handle, mpv))
}
#[tokio::test]
#[cfg(target_family = "unix")]
async fn test_get_mpv_version() {
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
let version: String = mpv.get_property("mpv-version").await.unwrap();
assert!(version.starts_with("mpv"));
mpv.kill().await.unwrap();
proc.kill().await.unwrap();
}
#[tokio::test]
#[cfg(target_family = "unix")]
async fn test_set_property() {
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
mpv.set_property("pause", true).await.unwrap();
let paused: bool = mpv.get_property("pause").await.unwrap();
assert_eq!(paused, true);
mpv.kill().await.unwrap();
proc.kill().await.unwrap();
}