Compare commits

..

1 Commits
main ... master

Author SHA1 Message Date
6b967371fe
.envrc: init
Some checks failed
Build and test / build (push) Has been cancelled
Build and test / check (push) Has been cancelled
Build and test / test (push) Has been cancelled
Build and test / docs (push) Has been cancelled
2024-08-01 17:57:28 +02:00
21 changed files with 556 additions and 909 deletions

View File

@ -2,31 +2,42 @@ name: "Build and test"
on:
push:
branches:
- main
- master
pull_request:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-latest-personal
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --all-features --verbose --release
check:
runs-on: ubuntu-latest
runs-on: ubuntu-latest-personal
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
components: rustfmt, clippy
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Check code format
run: cargo fmt -- --check
@ -34,27 +45,30 @@ jobs:
run: cargo clippy --all-features -- --deny warnings
test:
runs-on: ubuntu-latest
runs-on: ubuntu-latest-personal
steps:
- uses: actions/checkout@v4
- name: Install cargo binstall
uses: cargo-bins/cargo-binstall@main
- uses: actions/checkout@v3
- uses: cargo-bins/cargo-binstall@main
- name: Install mpv
run: apt-get update && apt-get install -y mpv
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@nightly
- name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
components: llvm-tools-preview
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Install nextest
run: cargo binstall -y cargo-nextest --secure
- name: Run tests
run: |
cargo nextest run --all-features --release --no-fail-fast --test-threads=1
cargo nextest run --all-features --release --no-fail-fast
env:
RUST_LOG: "trace"
RUSTFLAGS: "-Cinstrument-coverage"
@ -82,32 +96,38 @@ jobs:
target/coverage/
- name: Upload test report
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
uses: https://git.pvv.ntnu.no/oysteikt/rsync-action@main
with:
source: target/coverage/html/
target: ${{ gitea.ref_name }}/coverage/
username: gitea-web
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
host: bekkalokk.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx"
target: mpvipc/${{ gitea.ref_name }}/coverage/
username: oysteikt
ssh-key: ${{ secrets.OYSTEIKT_GITEA_WEBDOCS_SSH_KEY }}
host: microbel.pvv.ntnu.no
known-hosts: "microbel.pvv.ntnu.no ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEq0yasKP0mH6PI6ypmuzPzMnbHELo9k+YB5yW534aKudKZS65YsHJKQ9vapOtmegrn5MQbCCgrshf+/XwZcjbM="
docs:
runs-on: ubuntu-latest
runs-on: ubuntu-latest-personal
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Build docs
run: cargo doc --all-features --document-private-items --release
- name: Transfer files
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
uses: https://git.pvv.ntnu.no/oysteikt/rsync-action@main
with:
source: target/doc/
target: ${{ gitea.ref_name }}/docs/
username: gitea-web
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
host: bekkalokk.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx"
target: mpvipc/${{ gitea.ref_name }}/docs/
username: oysteikt
ssh-key: ${{ secrets.OYSTEIKT_GITEA_WEBDOCS_SSH_KEY }}
host: microbel.pvv.ntnu.no
known-hosts: "microbel.pvv.ntnu.no ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEq0yasKP0mH6PI6ypmuzPzMnbHELo9k+YB5yW534aKudKZS65YsHJKQ9vapOtmegrn5MQbCCgrshf+/XwZcjbM="

View File

@ -1,14 +1,15 @@
[package]
name = "mpvipc-async"
version = "0.1.0"
name = "mpvipc"
version = "1.3.0"
authors = [
"Jonas Frei <freijon@pm.me>",
"Øystein Tveit <oysteikt@pvv.ntnu.no>"
"h7x4 <h7x4@nani.wtf>"
]
description = "A small library which provides bindings to control existing mpv instances through sockets."
license = "GPL-3.0"
repository = "https://git.pvv.ntnu.no/Grzegorz/mpvipc-async"
documentation = "https://pages.pvv.ntnu.no/Grzegorz/mpvipc-async/main/docs/mpvipc_async/"
homepage = "https://git.pvv.ntnu.no/oysteikt/mpvipc"
repository = "https://git.pvv.ntnu.no/oysteikt/mpvipc"
documentation = "https://pvv.ntnu.no/~oysteikt/gitea/mpvipc/master/docs/mpvipc/"
edition = "2021"
rust-version = "1.75"

View File

@ -1,18 +1,16 @@
[![Coverage](https://pages.pvv.ntnu.no/Grzegorz/mpvipc-async/main/coverage/badges/for_the_badge.svg)](https://pages.pvv.ntnu.no/Grzegorz/mpvipc-async/main/coverage/src/)
[![Docs](https://img.shields.io/badge/docs-blue?style=for-the-badge&logo=rust)](https://pages.pvv.ntnu.no/Grzegorz/mpvipc-async/main/docs/mpvipc_async/)
# mpvipc-async
> **NOTE:** This is a fork of [gitlab.com/mpv-ipc/mpvipc](https://gitlab.com/mpv-ipc/mpvipc), which introduces a lot of changes to be able to use the library asynchronously with [tokio](https://github.com/tokio-rs/tokio).
[![Coverage](https://pvv.ntnu.no/~oysteikt/gitea/mpvipc/master/coverage/badges/for_the_badge.svg)](https://pvv.ntnu.no/~oysteikt/gitea/mpvipc/master/coverage/src/)
[![Docs](https://img.shields.io/badge/docs-blue?style=for-the-badge&logo=rust)](https://pvv.ntnu.no/~oysteikt/gitea/mpvipc/master/docs/mpvipc/)
# mpvipc
A small library which provides bindings to control existing mpv instances through sockets.
## Dependencies
- `mpv` (runtime dependency)
- `cargo-nextest` (optional test depencency)
- `grcov` (optional test depencency)
- `mpv`
- `cargo` (make dependency)
- `cargo-nextest` (test depencency)
- `grcov` (test depencency)
## Example
@ -25,12 +23,12 @@ $ mpv --input-ipc-server=/tmp/mpv.sock --idle
Here is a small code example which connects to the socket `/tmp/mpv.sock` and toggles playback.
```rust
use mpvipc_async::*;
use mpvipc::*;
#[tokio::main]
async fn main() -> Result<(), MpvError> {
let mpv = Mpv::connect("/tmp/mpv.sock").await?;
let paused: bool = mpv.get_property("pause").await?;
mpv.set_property("pause", !paused).await.expect("Error pausing");
mpv.set_property("pause", !paused).expect("Error pausing");
}
```
```

View File

@ -1,4 +1,4 @@
use mpvipc_async::{Mpv, MpvError, MpvExt};
use mpvipc::{Mpv, MpvError, MpvExt};
#[tokio::main]
async fn main() -> Result<(), MpvError> {
@ -12,8 +12,8 @@ async fn main() -> Result<(), MpvError> {
let playlist = mpv.get_playlist().await?;
println!("playlist: {:?}", playlist);
let playback_time: Option<f64> = mpv.get_property("playback-time").await?;
println!("playback-time: {:?}", playback_time);
let playback_time: f64 = mpv.get_property("playback-time").await?;
println!("playback-time: {}", playback_time);
Ok(())
}

View File

@ -1,5 +1,5 @@
use futures::StreamExt;
use mpvipc_async::{parse_property, Event, Mpv, MpvDataType, MpvError, MpvExt, Property};
use mpvipc::{parse_event_property, Event, Mpv, MpvDataType, MpvError, MpvExt, Property};
fn seconds_to_hms(total: f64) -> String {
let total = total as u64;
@ -25,36 +25,34 @@ async fn main() -> Result<(), MpvError> {
let mut events = mpv.get_event_stream().await;
while let Some(Ok(event)) = events.next().await {
match event {
mpvipc_async::Event::PropertyChange { name, data, .. } => {
match parse_property(&name, data)? {
Property::Path(Some(value)) => println!("\nPlaying: {}", value),
Property::Pause(value) => {
println!("Pause: {}", value);
}
Property::PlaybackTime(Some(value)) => {
println!("Playback time: {}", seconds_to_hms(value));
}
Property::Duration(Some(value)) => {
println!("Duration: {}", seconds_to_hms(value));
}
Property::Metadata(Some(value)) => {
println!("File tags:");
if let Some(MpvDataType::String(value)) = value.get("ARTIST") {
println!(" Artist: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("ALBUM") {
println!(" Album: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("TITLE") {
println!(" Title: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("TRACK") {
println!(" Track: {}", value);
}
}
_ => (),
mpvipc::Event::PropertyChange { .. } => match parse_event_property(event)? {
(1, Property::Path(Some(value))) => println!("\nPlaying: {}", value),
(2, Property::Pause(value)) => {
println!("Pause: {}", value);
}
}
(3, Property::PlaybackTime(Some(value))) => {
println!("Playback time: {}", seconds_to_hms(value));
}
(4, Property::Duration(Some(value))) => {
println!("Duration: {}", seconds_to_hms(value));
}
(5, Property::Metadata(Some(value))) => {
println!("File tags:");
if let Some(MpvDataType::String(value)) = value.get("ARTIST") {
println!(" Artist: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("ALBUM") {
println!(" Album: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("TITLE") {
println!(" Title: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("TRACK") {
println!(" Track: {}", value);
}
}
_ => (),
},
Event::Shutdown => return Ok(()),
Event::Unimplemented(_) => panic!("Unimplemented event"),
_ => (),

View File

@ -5,12 +5,12 @@
fenix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, fenix }@inputs:
let
let
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: let
@ -30,9 +30,8 @@
])
pkgs.mpv
pkgs.grcov
pkgs.cargo-nextest
];
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library";
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/";
});
};
}

View File

@ -27,65 +27,39 @@ use crate::{
/// the upstream list of commands.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MpvCommand {
/// Load the given file or URL and play it.
LoadFile {
file: String,
option: PlaylistAddOptions,
},
/// Load the given playlist file or URL.
LoadList {
file: String,
option: PlaylistAddOptions,
},
/// Clear the playlist, except for the currently playing file.
PlaylistClear,
///Move the playlist entry at `from`, so that it takes the place of the entry `to`.
/// (Paradoxically, the moved playlist entry will not have the index value `to` after moving
/// if `from` was lower than `to`, because `to` refers to the target entry, not the index
/// the entry will have after moving.)
PlaylistMove { from: usize, to: usize },
/// Observe a property. This will start triggering [`Event::PropertyChange`] events
/// in the event stream whenever the specific property changes.
/// You can use [`Mpv::get_event_stream`] to get the stream.
Observe { id: u64, property: String },
/// Skip to the next entry in the playlist.
PlaylistMove {
from: usize,
to: usize,
},
Observe {
id: usize,
property: String,
},
PlaylistNext,
/// Skip to the previous entry in the playlist.
PlaylistPrev,
/// Remove an entry from the playlist by its position in the playlist.
PlaylistRemove(usize),
/// Shuffle the playlist
PlaylistShuffle,
/// Exit the player
Quit,
/// Send a message to all clients, and pass it the following list of arguments.
/// What this message means, how many arguments it takes, and what the arguments
/// mean is fully up to the receiver and the sender.
ScriptMessage(Vec<String>),
/// Same as [`MpvCommand::ScriptMessage`], but send the message to a specific target.
ScriptMessageTo { target: String, args: Vec<String> },
/// Change the playback position.
Seek { seconds: f64, option: SeekOptions },
/// Stop the current playback, and clear the playlist.
/// This esentially resets the entire player state without exiting mpv.
ScriptMessageTo {
target: String,
args: Vec<String>,
},
Seek {
seconds: f64,
option: SeekOptions,
},
Stop,
/// Unobserve all properties registered with the given id.
/// See [`MpvCommand::Observe`] for more context.
Unobserve(u64),
Unobserve(usize),
}
/// Helper trait to keep track of the string literals that mpv expects.
@ -95,7 +69,6 @@ pub(crate) trait IntoRawCommandPart {
/// Generic data type representing all possible data types that mpv can return.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MpvDataType {
Array(Vec<MpvDataType>),
Bool(bool),
@ -109,7 +82,7 @@ pub enum MpvDataType {
}
/// A mpv playlist.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Playlist(pub Vec<PlaylistEntry>);
/// A single entry in the mpv playlist.
@ -117,7 +90,7 @@ pub struct Playlist(pub Vec<PlaylistEntry>);
pub struct PlaylistEntry {
pub id: usize,
pub filename: String,
pub title: Option<String>,
pub title: String,
pub current: bool,
}
@ -161,22 +134,18 @@ impl IntoRawCommandPart for SeekOptions {
pub trait GetPropertyTypeHandler: Sized {
// TODO: fix this
#[allow(async_fn_in_trait)]
async fn get_property_generic(instance: &Mpv, property: &str)
-> Result<Option<Self>, MpvError>;
async fn get_property_generic(instance: &Mpv, property: &str) -> Result<Self, MpvError>;
}
impl<T> GetPropertyTypeHandler for T
where
T: TypeHandler,
{
async fn get_property_generic(instance: &Mpv, property: &str) -> Result<Option<T>, MpvError> {
async fn get_property_generic(instance: &Mpv, property: &str) -> Result<T, MpvError> {
instance
.get_property_value(property)
.await
.and_then(|value| match value {
Some(v) => T::get_value(v).map(|v| Some(v)),
None => Ok(None),
})
.and_then(T::get_value)
}
}
@ -203,7 +172,7 @@ where
instance
.command_sender
.send((
MpvIpcCommand::SetProperty(property.to_owned(), value.to_owned()),
MpvIpcCommand::SetProperty(property.to_owned(), value),
res_tx,
))
.await
@ -308,7 +277,7 @@ impl Mpv {
command: &str,
args: &[&str],
) -> Result<Option<Value>, MpvError> {
let command_vec = Vec::from(
let command = Vec::from(
[command]
.iter()
.chain(args.iter())
@ -318,7 +287,7 @@ impl Mpv {
);
let (res_tx, res_rx) = oneshot::channel();
self.command_sender
.send((MpvIpcCommand::Command(command_vec.clone()), res_tx))
.send((MpvIpcCommand::Command(command), res_tx))
.await
.map_err(|err| MpvError::InternalConnectionError(err.to_string()))?;
@ -348,7 +317,7 @@ impl Mpv {
///
/// # Example
/// ```
/// use mpvipc_async::{Mpv, MpvError};
/// use mpvipc::{Mpv, MpvError};
///
/// #[tokio::main]
/// async fn main() -> Result<(), MpvError> {
@ -480,7 +449,7 @@ impl Mpv {
///
/// # Example
/// ```
/// use mpvipc_async::{Mpv, MpvError};
/// use mpvipc::{Mpv, MpvError};
///
/// #[tokio::main]
/// async fn main() -> Result<(), MpvError> {
@ -493,7 +462,7 @@ impl Mpv {
pub async fn get_property<T: GetPropertyTypeHandler>(
&self,
property: &str,
) -> Result<Option<T>, MpvError> {
) -> Result<T, MpvError> {
T::get_property_generic(self, property).await
}
@ -509,7 +478,7 @@ impl Mpv {
/// # Example
///
/// ```
/// use mpvipc_async::{Mpv, MpvError};
/// use mpvipc::{Mpv, MpvError};
///
/// #[tokio::main]
/// async fn main() -> Result<(), MpvError> {
@ -518,7 +487,7 @@ impl Mpv {
/// Ok(())
/// }
/// ```
pub async fn get_property_value(&self, property: &str) -> Result<Option<Value>, MpvError> {
pub async fn get_property_value(&self, property: &str) -> Result<Value, MpvError> {
let (res_tx, res_rx) = oneshot::channel();
self.command_sender
.send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx))
@ -526,7 +495,9 @@ impl Mpv {
.map_err(|err| MpvError::InternalConnectionError(err.to_string()))?;
match res_rx.await {
Ok(MpvIpcResponse(response)) => response,
Ok(MpvIpcResponse(response)) => {
response.and_then(|value| value.ok_or(MpvError::MissingMpvData))
}
Err(err) => Err(MpvError::InternalConnectionError(err.to_string())),
}
}
@ -548,17 +519,18 @@ impl Mpv {
///
/// # Example
/// ```
/// use mpvipc_async::{Mpv, MpvError};
/// use mpvipc::{Mpv, MpvError};
/// async fn main() -> Result<(), MpvError> {
/// let mpv = Mpv::connect("/tmp/mpvsocket").await?;
/// mpv.set_property("pause", true).await?;
/// Ok(())
/// }
/// ```
pub async fn set_property<T>(&self, property: &str, value: T) -> Result<(), MpvError>
where
T: SetPropertyTypeHandler<T> + Clone + fmt::Debug,
{
T::set_property_generic(self, property, value.clone()).await
pub async fn set_property<T: SetPropertyTypeHandler<T>>(
&self,
property: &str,
value: T,
) -> Result<(), MpvError> {
T::set_property_generic(self, property, value).await
}
}

View File

@ -3,16 +3,13 @@
use serde_json::{Map, Value};
use thiserror::Error;
use crate::{MpvDataType, Property};
use crate::MpvDataType;
/// Any error that can occur when interacting with mpv.
#[derive(Error, Debug)]
pub enum MpvError {
#[error("Mpv returned error in response to command: {message}\nCommand: {command:#?}")]
MpvError {
command: Vec<Value>,
message: String,
},
#[error("MpvError: {0}")]
MpvError(String),
#[error("Error communicating over mpv socket: {0}")]
MpvSocketConnectionError(String),
@ -46,63 +43,6 @@ pub enum MpvError {
map: Map<String, Value>,
},
#[error("Unexpected property: {0:?}")]
UnexpectedProperty(Property),
#[error("Unknown error: {0}")]
Other(String),
}
impl PartialEq for MpvError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(
Self::MpvError {
command: l_command,
message: l_message,
},
Self::MpvError {
command: r_command,
message: r_message,
},
) => l_command == r_command && l_message == r_message,
(Self::MpvSocketConnectionError(l0), Self::MpvSocketConnectionError(r0)) => l0 == r0,
(Self::InternalConnectionError(l0), Self::InternalConnectionError(r0)) => l0 == r0,
(Self::JsonParseError(l0), Self::JsonParseError(r0)) => {
l0.to_string() == r0.to_string()
}
(
Self::ValueContainsUnexpectedType {
expected_type: l_expected_type,
received: l_received,
},
Self::ValueContainsUnexpectedType {
expected_type: r_expected_type,
received: r_received,
},
) => l_expected_type == r_expected_type && l_received == r_received,
(
Self::DataContainsUnexpectedType {
expected_type: l_expected_type,
received: l_received,
},
Self::DataContainsUnexpectedType {
expected_type: r_expected_type,
received: r_received,
},
) => l_expected_type == r_expected_type && l_received == r_received,
(
Self::MissingKeyInObject {
key: l_key,
map: l_map,
},
Self::MissingKeyInObject {
key: r_key,
map: r_map,
},
) => l_key == r_key && l_map == r_map,
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}

View File

@ -109,7 +109,7 @@ pub enum Event {
VideoReconfig,
AudioReconfig,
PropertyChange {
id: Option<u64>,
id: usize,
name: String,
data: Option<MpvDataType>,
},
@ -209,12 +209,6 @@ pub(crate) fn parse_event(raw_event: MpvIpcEvent) -> Result<Event, MpvError> {
"shutdown" => Ok(Event::Shutdown),
"log-message" => parse_log_message(event),
"hook" => parse_hook(event),
// TODO: fix these. They are asynchronous responses to different requests.
// see:
// - https://github.com/mpv-player/mpv/blob/5f768a688b706cf94041adf5bed7c7004af2ec5a/libmpv/client.h#L1158-L1160
// - https://github.com/mpv-player/mpv/blob/5f768a688b706cf94041adf5bed7c7004af2ec5a/libmpv/client.h#L1095-L1098
// - https://github.com/mpv-player/mpv/blob/5f768a688b706cf94041adf5bed7c7004af2ec5a/libmpv/client.h#L972-L982
// "get-property-reply" =>
// "set-property-reply" =>
// "command-reply" =>
@ -296,7 +290,7 @@ fn parse_client_message(event: &Map<String, Value>) -> Result<Event, MpvError> {
}
fn parse_property_change(event: &Map<String, Value>) -> Result<Event, MpvError> {
let id = get_optional_key_as!(as_u64, "id", event);
let id = get_key_as!(as_u64, "id", event) as usize;
let property_name = get_key_as!(as_str, "name", event);
let data = event.get("data").map(json_to_value).transpose()?;

View File

@ -1,24 +1,21 @@
//! JSON parsing logic for properties returned by
//! [`Event::PropertyChange`], and used internally in `MpvExt`
//! to parse the response from `Mpv::get_property()`.
//! JSON parsing logic for properties returned in [`Event::PropertyChange`]
//!
//! This module is used to parse the json data from the `data` field of
//! known properties. Mpv has about 1000 different properties
//! 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.
// TODO: reuse this logic for providing a more typesafe response API to `Mpv::get_property()`
// Although this data is currently of type `Option<`
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{MpvDataType, MpvError, PlaylistEntry};
use crate::{Event, MpvDataType, MpvError, PlaylistEntry};
/// An incomplete list of properties that mpv can return.
/// All possible properties that can be observed through the event system.
///
/// Unimplemented properties will be returned with it's data
/// as a `Property::Unknown` variant.
/// 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.
@ -34,12 +31,9 @@ pub enum Property {
PlaylistPos(Option<usize>),
LoopFile(LoopProperty),
LoopPlaylist(LoopProperty),
TimePos(Option<f64>),
TimeRemaining(Option<f64>),
Speed(f64),
Volume(f64),
Mute(bool),
EofReached(bool),
Unknown {
name: String,
data: Option<MpvDataType>,
@ -54,12 +48,17 @@ pub enum LoopProperty {
No,
}
/// Parse a highlevel [`Property`] object from mpv data.
///
/// This is intended to be used with the `data` field of
/// `Event::PropertyChange` and the response from `Mpv::get_property_value()`.
pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property, MpvError> {
match name {
/// Parse a highlevel [`Property`] object from json, used for [`Event::PropertyChange`].
pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError> {
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 {
Some(MpvDataType::String(s)) => Some(s),
@ -74,7 +73,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
return Err(MpvError::MissingMpvData);
}
};
Ok(Property::Path(path))
Ok((id, Property::Path(path)))
}
"pause" => {
let pause = match data {
@ -89,7 +88,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
return Err(MpvError::MissingMpvData);
}
};
Ok(Property::Pause(pause))
Ok((id, Property::Pause(pause)))
}
"playback-time" => {
let playback_time = match data {
@ -102,7 +101,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
})
}
};
Ok(Property::PlaybackTime(playback_time))
Ok((id, Property::PlaybackTime(playback_time)))
}
"duration" => {
let duration = match data {
@ -115,7 +114,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
})
}
};
Ok(Property::Duration(duration))
Ok((id, Property::Duration(duration)))
}
"metadata" => {
let metadata = match data {
@ -128,21 +127,16 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
})
}
};
Ok(Property::Metadata(metadata))
}
"playlist" => {
let playlist = match data {
Some(MpvDataType::Array(a)) => mpv_array_to_playlist(&a)?,
None => Vec::new(),
Some(data) => {
return Err(MpvError::DataContainsUnexpectedType {
expected_type: "Array".to_owned(),
received: data,
})
}
};
Ok(Property::Playlist(playlist))
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 {
Some(MpvDataType::Usize(u)) => Some(u),
@ -156,7 +150,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
})
}
};
Ok(Property::PlaylistPos(playlist_pos))
Ok((id, Property::PlaylistPos(playlist_pos)))
}
"loop-file" => {
let loop_file = match data.to_owned() {
@ -178,7 +172,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
},
None => MpvError::MissingMpvData,
})?;
Ok(Property::LoopFile(loop_file))
Ok((id, Property::LoopFile(loop_file)))
}
"loop-playlist" => {
let loop_playlist = match data.to_owned() {
@ -201,34 +195,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
None => MpvError::MissingMpvData,
})?;
Ok(Property::LoopPlaylist(loop_playlist))
}
"time-pos" => {
let time_pos = match data {
Some(MpvDataType::Double(d)) => Some(d),
Some(data) => {
return Err(MpvError::DataContainsUnexpectedType {
expected_type: "f64".to_owned(),
received: data,
})
}
None => None,
};
Ok(Property::TimePos(time_pos))
}
"time-remaining" => {
let time_remaining = match data {
Some(MpvDataType::Double(d)) => Some(d),
Some(data) => {
return Err(MpvError::DataContainsUnexpectedType {
expected_type: "f64".to_owned(),
received: data,
})
}
None => None,
};
Ok(Property::TimeRemaining(time_remaining))
Ok((id, Property::LoopPlaylist(loop_playlist)))
}
"speed" => {
let speed = match data {
@ -243,7 +210,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
return Err(MpvError::MissingMpvData);
}
};
Ok(Property::Speed(speed))
Ok((id, Property::Speed(speed)))
}
"volume" => {
let volume = match data {
@ -258,7 +225,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
return Err(MpvError::MissingMpvData);
}
};
Ok(Property::Volume(volume))
Ok((id, Property::Volume(volume)))
}
"mute" => {
let mute = match data {
@ -273,81 +240,9 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
return Err(MpvError::MissingMpvData);
}
};
Ok(Property::Mute(mute))
}
"eof-reached" => {
let eof_reached = match data {
Some(MpvDataType::Bool(b)) => b,
Some(data) => {
return Err(MpvError::DataContainsUnexpectedType {
expected_type: "bool".to_owned(),
received: data,
})
}
None => true,
};
Ok(Property::EofReached(eof_reached))
Ok((id, Property::Mute(mute)))
}
// TODO: add missing cases
_ => Ok(Property::Unknown {
name: name.to_owned(),
data,
}),
_ => Ok((id, Property::Unknown { name, data })),
}
}
fn mpv_data_to_playlist_entry(
map: &HashMap<String, MpvDataType>,
) -> Result<PlaylistEntry, MpvError> {
let filename = match map.get("filename") {
Some(MpvDataType::String(s)) => s.to_string(),
Some(data) => {
return Err(MpvError::DataContainsUnexpectedType {
expected_type: "String".to_owned(),
received: data.clone(),
})
}
None => return Err(MpvError::MissingMpvData),
};
let title = match map.get("title") {
Some(MpvDataType::String(s)) => Some(s.to_string()),
Some(data) => {
return Err(MpvError::DataContainsUnexpectedType {
expected_type: "String".to_owned(),
received: data.clone(),
})
}
None => None,
};
let current = match map.get("current") {
Some(MpvDataType::Bool(b)) => *b,
Some(data) => {
return Err(MpvError::DataContainsUnexpectedType {
expected_type: "bool".to_owned(),
received: data.clone(),
})
}
None => false,
};
Ok(PlaylistEntry {
id: 0,
filename,
title,
current,
})
}
fn mpv_array_to_playlist(array: &[MpvDataType]) -> Result<Vec<PlaylistEntry>, MpvError> {
array
.iter()
.map(|value| match value {
MpvDataType::HashMap(map) => mpv_data_to_playlist_entry(map),
_ => Err(MpvError::DataContainsUnexpectedType {
expected_type: "HashMap".to_owned(),
received: value.clone(),
}),
})
.enumerate()
.map(|(id, entry)| entry.map(|entry| PlaylistEntry { id, ..entry }))
.collect()
}

View File

@ -1,8 +1,8 @@
//! High-level API extension for [`Mpv`].
use crate::{
parse_property, IntoRawCommandPart, LoopProperty, Mpv, MpvCommand, MpvDataType, MpvError,
Playlist, PlaylistAddOptions, Property, SeekOptions,
IntoRawCommandPart, Mpv, MpvCommand, MpvDataType, MpvError, Playlist, PlaylistAddOptions,
PlaylistEntry, SeekOptions,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@ -44,7 +44,35 @@ pub enum PlaylistAddTypeOptions {
// TODO: fix this
#[allow(async_fn_in_trait)]
pub trait MpvExt {
// COMMANDS
/// Stop the player completely (as opposed to pausing),
/// removing the pointer to the current video.
async fn stop(&self) -> Result<(), MpvError>;
/// Set the volume of the player.
async fn set_volume(
&self,
input_volume: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError>;
/// Set the playback speed of the player.
async fn set_speed(
&self,
input_speed: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError>;
/// Toggle/set the pause state of the player.
async fn set_playback(&self, option: Switch) -> Result<(), MpvError>;
/// Toggle/set the mute state of the player.
async fn set_mute(&self, option: Switch) -> Result<(), MpvError>;
/// Toggle/set whether the player should loop the current playlist.
async fn set_loop_playlist(&self, option: Switch) -> Result<(), MpvError>;
/// Toggle/set whether the player should loop the current video.
async fn set_loop_file(&self, option: Switch) -> Result<(), MpvError>;
/// Seek to a specific position in the current video.
async fn seek(&self, seconds: f64, option: SeekOptions) -> Result<(), MpvError>;
@ -88,11 +116,11 @@ pub trait MpvExt {
/// Notify mpv to send events whenever a property changes.
/// See [`Mpv::get_event_stream`] and [`Property`](crate::Property) for more information.
async fn observe_property(&self, id: u64, property: &str) -> Result<(), MpvError>;
async fn observe_property(&self, id: usize, property: &str) -> Result<(), MpvError>;
/// Stop observing a property.
/// See [`Mpv::get_event_stream`] and [`Property`](crate::Property) for more information.
async fn unobserve_property(&self, id: u64) -> Result<(), MpvError>;
async fn unobserve_property(&self, id: usize) -> Result<(), MpvError>;
/// Skip to the next entry in the playlist.
async fn next(&self) -> Result<(), MpvError>;
@ -104,122 +132,71 @@ pub trait MpvExt {
/// it to exit itself. If mpv is stuck, it may not respond to this command.
async fn kill(&self) -> Result<(), MpvError>;
/// Stop the player completely (as opposed to pausing),
/// removing the pointer to the current video.
async fn stop(&self) -> Result<(), MpvError>;
// SETTERS
/// Set the volume of the player.
async fn set_volume(
&self,
input_volume: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError>;
/// Set the playback speed of the player.
async fn set_speed(
&self,
input_speed: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError>;
/// Toggle/set the pause state of the player.
async fn set_playback(&self, option: Switch) -> Result<(), MpvError>;
/// Toggle/set the mute state of the player.
async fn set_mute(&self, option: Switch) -> Result<(), MpvError>;
/// Toggle/set whether the player should loop the current playlist.
async fn set_loop_playlist(&self, option: Switch) -> Result<(), MpvError>;
/// Toggle/set whether the player should loop the current video.
async fn set_loop_file(&self, option: Switch) -> Result<(), MpvError>;
// GETTERS
/// Get a list of all entries in the playlist.
async fn get_playlist(&self) -> Result<Playlist, MpvError>;
/// Get metadata about the current video.
async fn get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError>;
/// Get the path of the current video.
async fn get_file_path(&self) -> Result<String, MpvError>;
/// Get the current volume of the player.
async fn get_volume(&self) -> Result<f64, MpvError>;
/// Get the playback speed of the player.
async fn get_speed(&self) -> Result<f64, MpvError>;
/// Get the current position in the current video.
async fn get_time_pos(&self) -> Result<Option<f64>, MpvError>;
/// Get the amount of time remaining in the current video.
async fn get_time_remaining(&self) -> Result<Option<f64>, MpvError>;
/// Get the total duration of the current video.
async fn get_duration(&self) -> Result<f64, MpvError>;
/// Get the current position in the playlist.
async fn get_playlist_pos(&self) -> Result<usize, MpvError>;
// BOOLEAN GETTERS
/// Check whether the player is muted.
async fn is_muted(&self) -> Result<bool, MpvError>;
/// Check whether the player is currently playing.
async fn is_playing(&self) -> Result<bool, MpvError>;
/// Check whether the player is looping the current playlist.
async fn playlist_is_looping(&self) -> Result<LoopProperty, MpvError>;
/// Check whether the player is looping the current video.
async fn file_is_looping(&self) -> Result<LoopProperty, MpvError>;
}
impl MpvExt for Mpv {
// COMMANDS
async fn seek(&self, seconds: f64, option: SeekOptions) -> Result<(), MpvError> {
self.run_command(MpvCommand::Seek { seconds, option }).await
async fn get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError> {
self.get_property("metadata").await
}
async fn playlist_shuffle(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistShuffle).await
async fn get_playlist(&self) -> Result<Playlist, MpvError> {
self.get_property::<Vec<PlaylistEntry>>("playlist")
.await
.map(Playlist)
}
async fn playlist_remove_id(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistRemove(id)).await
async fn kill(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Quit).await
}
async fn playlist_play_next(&self, id: usize) -> Result<(), MpvError> {
let data = self.get_property("playlist-pos").await?;
let current_id = match parse_property("playlist-pos", data)? {
Property::PlaylistPos(Some(current_id)) => Ok(current_id),
prop => Err(MpvError::UnexpectedProperty(prop)),
}?;
async fn next(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistNext).await
}
self.run_command(MpvCommand::PlaylistMove {
from: id,
to: current_id + 1,
async fn observe_property(&self, id: usize, property: &str) -> Result<(), MpvError> {
self.run_command(MpvCommand::Observe {
id,
property: property.to_string(),
})
.await
}
async fn playlist_play_id(&self, id: usize) -> Result<(), MpvError> {
self.set_property("playlist-pos", id).await
async fn unobserve_property(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::Unobserve(id)).await
}
async fn playlist_move_id(&self, from: usize, to: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistMove { from, to })
.await
async fn set_playback(&self, option: Switch) -> Result<(), MpvError> {
let enabled = match option {
Switch::On => "yes",
Switch::Off => "no",
Switch::Toggle => {
self.get_property::<String>("pause")
.await
.map(|s| match s.as_str() {
"yes" => "no",
"no" => "yes",
_ => "no",
})?
}
};
self.set_property("pause", enabled).await
}
async fn playlist_clear(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistClear).await
async fn prev(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistPrev).await
}
async fn restart(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Seek {
seconds: 0f64,
option: SeekOptions::Absolute,
})
.await
}
async fn playlist_add(
@ -247,89 +224,76 @@ impl MpvExt for Mpv {
}
}
async fn restart(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Seek {
seconds: 0f64,
option: SeekOptions::Absolute,
})
.await
async fn playlist_clear(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistClear).await
}
async fn prev(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistPrev).await
async fn playlist_move_id(&self, from: usize, to: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistMove { from, to })
.await
}
async fn observe_property(&self, id: u64, property: &str) -> Result<(), MpvError> {
self.run_command(MpvCommand::Observe {
id,
property: property.to_string(),
})
.await
async fn playlist_play_id(&self, id: usize) -> Result<(), MpvError> {
self.set_property("playlist-pos", id).await
}
async fn unobserve_property(&self, id: u64) -> Result<(), MpvError> {
self.run_command(MpvCommand::Unobserve(id)).await
}
async fn next(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistNext).await
}
async fn kill(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Quit).await
}
async fn stop(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Stop).await
}
// SETTERS
async fn set_volume(
&self,
input_volume: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError> {
let volume = self.get_volume().await?;
match option {
NumberChangeOptions::Increase => {
self.set_property("volume", volume + input_volume).await
async fn playlist_play_next(&self, id: usize) -> Result<(), MpvError> {
match self.get_property::<usize>("playlist-pos").await {
Ok(current_id) => {
self.run_command(MpvCommand::PlaylistMove {
from: id,
to: current_id + 1,
})
.await
}
NumberChangeOptions::Decrease => {
self.set_property("volume", volume - input_volume).await
}
NumberChangeOptions::Absolute => self.set_property("volume", input_volume).await,
Err(msg) => Err(msg),
}
}
async fn set_speed(
&self,
input_speed: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError> {
let speed = self.get_speed().await?;
match option {
NumberChangeOptions::Increase => self.set_property("speed", speed + input_speed).await,
NumberChangeOptions::Decrease => self.set_property("speed", speed - input_speed).await,
NumberChangeOptions::Absolute => self.set_property("speed", input_speed).await,
}
async fn playlist_remove_id(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistRemove(id)).await
}
async fn set_playback(&self, option: Switch) -> Result<(), MpvError> {
async fn playlist_shuffle(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistShuffle).await
}
async fn seek(&self, seconds: f64, option: SeekOptions) -> Result<(), MpvError> {
self.run_command(MpvCommand::Seek { seconds, option }).await
}
async fn set_loop_file(&self, option: Switch) -> Result<(), MpvError> {
let enabled = match option {
Switch::On => "no",
Switch::Off => "yes",
Switch::On => "inf",
Switch::Off => "no",
Switch::Toggle => {
if self.is_playing().await? {
"yes"
} else {
"no"
}
self.get_property::<String>("loop-file")
.await
.map(|s| match s.as_str() {
"inf" => "no",
"no" => "inf",
_ => "no",
})?
}
};
self.set_property("pause", enabled).await
self.set_property("loop-file", enabled).await
}
async fn set_loop_playlist(&self, option: Switch) -> Result<(), MpvError> {
let enabled = match option {
Switch::On => "inf",
Switch::Off => "no",
Switch::Toggle => {
self.get_property::<String>("loop-playlist")
.await
.map(|s| match s.as_str() {
"inf" => "no",
"no" => "inf",
_ => "no",
})?
}
};
self.set_property("loo-playlist", enabled).await
}
async fn set_mute(&self, option: Switch) -> Result<(), MpvError> {
@ -337,147 +301,61 @@ impl MpvExt for Mpv {
Switch::On => "yes",
Switch::Off => "no",
Switch::Toggle => {
if self.is_muted().await? {
"no"
} else {
"yes"
}
self.get_property::<String>("mute")
.await
.map(|s| match s.as_str() {
"yes" => "no",
"no" => "yes",
_ => "no",
})?
}
};
self.set_property("mute", enabled).await
}
async fn set_loop_playlist(&self, option: Switch) -> Result<(), MpvError> {
let enabled = match option {
Switch::On => "inf",
Switch::Off => "no",
Switch::Toggle => match self.playlist_is_looping().await? {
LoopProperty::Inf => "no",
LoopProperty::N(_) => "no",
LoopProperty::No => "inf",
async fn set_speed(
&self,
input_speed: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError> {
match self.get_property::<f64>("speed").await {
Ok(speed) => match option {
NumberChangeOptions::Increase => {
self.set_property("speed", speed + input_speed).await
}
NumberChangeOptions::Decrease => {
self.set_property("speed", speed - input_speed).await
}
NumberChangeOptions::Absolute => self.set_property("speed", input_speed).await,
},
};
self.set_property("loop-playlist", enabled).await
Err(msg) => Err(msg),
}
}
async fn set_loop_file(&self, option: Switch) -> Result<(), MpvError> {
let enabled = match option {
Switch::On => "inf",
Switch::Off => "no",
Switch::Toggle => match self.file_is_looping().await? {
LoopProperty::Inf => "no",
LoopProperty::N(_) => "no",
LoopProperty::No => "inf",
async fn set_volume(
&self,
input_volume: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError> {
match self.get_property::<f64>("volume").await {
Ok(volume) => match option {
NumberChangeOptions::Increase => {
self.set_property("volume", volume + input_volume).await
}
NumberChangeOptions::Decrease => {
self.set_property("volume", volume - input_volume).await
}
NumberChangeOptions::Absolute => self.set_property("volume", input_volume).await,
},
};
self.set_property("loop-file", enabled).await
}
// GETTERS
async fn get_playlist(&self) -> Result<Playlist, MpvError> {
let data = self.get_property("playlist").await?;
match parse_property("playlist", data)? {
Property::Playlist(value) => Ok(Playlist(value)),
prop => Err(MpvError::UnexpectedProperty(prop)),
Err(msg) => Err(msg),
}
}
async fn get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError> {
let data = self.get_property("metadata").await?;
match parse_property("metadata", data)? {
Property::Metadata(Some(value)) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
async fn get_file_path(&self) -> Result<String, MpvError> {
let data = self.get_property("path").await?;
match parse_property("path", data)? {
Property::Path(Some(value)) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
async fn get_volume(&self) -> Result<f64, MpvError> {
let data = self.get_property("volume").await?;
match parse_property("volume", data)? {
Property::Volume(value) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
async fn get_speed(&self) -> Result<f64, MpvError> {
let data = self.get_property("speed").await?;
match parse_property("speed", data)? {
Property::Speed(value) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
async fn get_time_pos(&self) -> Result<Option<f64>, MpvError> {
let data = self.get_property("time-pos").await?;
match parse_property("time-pos", data)? {
Property::TimePos(value) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
async fn get_time_remaining(&self) -> Result<Option<f64>, MpvError> {
let data = self.get_property("time-remaining").await?;
match parse_property("time-remaining", data)? {
Property::TimeRemaining(value) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
async fn get_duration(&self) -> Result<f64, MpvError> {
let data = self.get_property("duration").await?;
match parse_property("duration", data)? {
Property::Duration(Some(value)) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
async fn get_playlist_pos(&self) -> Result<usize, MpvError> {
let data = self.get_property("playlist-pos").await?;
match parse_property("playlist-pos", data)? {
Property::PlaylistPos(Some(value)) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
// BOOLEAN GETTERS
async fn is_muted(&self) -> Result<bool, MpvError> {
let data = self.get_property("mute").await?;
match parse_property("mute", data)? {
Property::Mute(value) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
async fn is_playing(&self) -> Result<bool, MpvError> {
let data = self.get_property("pause").await?;
match parse_property("pause", data)? {
Property::Pause(value) => Ok(!value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
async fn playlist_is_looping(&self) -> Result<LoopProperty, MpvError> {
let data = self.get_property("loop-playlist").await?;
match parse_property("loop-playlist", data)? {
Property::LoopPlaylist(value) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
}
async fn file_is_looping(&self) -> Result<LoopProperty, MpvError> {
let data = self.get_property("loop-file").await?;
match parse_property("loop-file", data)? {
Property::LoopFile(value) => Ok(value),
prop => Err(MpvError::UnexpectedProperty(prop)),
}
async fn stop(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Stop).await
}
}

View File

@ -24,8 +24,8 @@ pub(crate) enum MpvIpcCommand {
Command(Vec<String>),
GetProperty(String),
SetProperty(String, Value),
ObserveProperty(u64, String),
UnobserveProperty(u64),
ObserveProperty(usize, String),
UnobserveProperty(usize),
Exit,
}
@ -92,7 +92,7 @@ impl MpvIpc {
log::trace!("Received response: {:?}", response);
parse_mpv_response_data(response?, command)
parse_mpv_response_data(response?)
}
pub(crate) async fn get_mpv_property(
@ -114,14 +114,17 @@ impl MpvIpc {
pub(crate) async fn observe_property(
&mut self,
id: u64,
id: usize,
property: &str,
) -> Result<Option<Value>, MpvError> {
self.send_command(&[json!("observe_property"), json!(id), json!(property)])
.await
}
pub(crate) async fn unobserve_property(&mut self, id: u64) -> Result<Option<Value>, MpvError> {
pub(crate) async fn unobserve_property(
&mut self,
id: usize,
) -> Result<Option<Value>, MpvError> {
self.send_command(&[json!("unobserve_property"), json!(id)])
.await
}
@ -194,7 +197,7 @@ 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, command: &[Value]) -> Result<Option<Value>, MpvError> {
fn parse_mpv_response_data(value: Value) -> Result<Option<Value>, MpvError> {
log::trace!("Parsing mpv response data: {:?}", value);
let result = value
.as_object()
@ -221,11 +224,7 @@ fn parse_mpv_response_data(value: Value, command: &[Value]) -> Result<Option<Val
})
.and_then(|(error, data)| match error {
"success" => Ok(data),
"property unavailable" => Ok(None),
err => Err(MpvError::MpvError {
command: command.to_owned(),
message: err.to_string(),
}),
err => Err(MpvError::MpvError(err.to_string())),
});
match &result {

View File

@ -3,13 +3,13 @@
mod core_api;
mod error;
mod event_parser;
mod event_property_parser;
mod highlevel_api_extension;
mod ipc;
mod message_parser;
mod property_parser;
pub use core_api::*;
pub use error::*;
pub use event_parser::*;
pub use event_property_parser::*;
pub use highlevel_api_extension::*;
pub use property_parser::*;

View File

@ -109,7 +109,7 @@ impl TypeHandler for Vec<PlaylistEntry> {
expected_type: "Array<Value>".to_string(),
received: value.clone(),
})
.and_then(|array| json_array_to_playlist(array))
.map(|array| json_array_to_playlist(array))
}
fn as_string(&self) -> String {
@ -155,65 +155,29 @@ pub(crate) fn json_array_to_vec(array: &[Value]) -> Result<Vec<MpvDataType>, Mpv
array.iter().map(json_to_value).collect()
}
fn json_map_to_playlist_entry(
map: &serde_json::map::Map<String, Value>,
) -> Result<PlaylistEntry, MpvError> {
let filename = match map.get("filename") {
Some(Value::String(s)) => s.to_string(),
Some(data) => {
return Err(MpvError::ValueContainsUnexpectedType {
expected_type: "String".to_owned(),
received: data.clone(),
})
pub(crate) fn json_array_to_playlist(array: &[Value]) -> Vec<PlaylistEntry> {
let mut output: Vec<PlaylistEntry> = Vec::new();
for (id, entry) in array.iter().enumerate() {
let mut filename: String = String::new();
let mut title: String = String::new();
let mut current: bool = false;
if let Value::String(ref f) = entry["filename"] {
filename = f.to_string();
}
None => return Err(MpvError::MissingMpvData),
};
let title = match map.get("title") {
Some(Value::String(s)) => Some(s.to_string()),
Some(data) => {
return Err(MpvError::ValueContainsUnexpectedType {
expected_type: "String".to_owned(),
received: data.clone(),
})
if let Value::String(ref t) = entry["title"] {
title = t.to_string();
}
None => None,
};
let current = match map.get("current") {
Some(Value::Bool(b)) => *b,
Some(data) => {
return Err(MpvError::ValueContainsUnexpectedType {
expected_type: "bool".to_owned(),
received: data.clone(),
})
if let Value::Bool(ref b) = entry["current"] {
current = *b;
}
None => false,
};
Ok(PlaylistEntry {
id: 0,
filename,
title,
current,
})
}
pub(crate) fn json_array_to_playlist(array: &[Value]) -> Result<Vec<PlaylistEntry>, MpvError> {
array
.iter()
.map(|entry| match entry {
Value::Object(map) => json_map_to_playlist_entry(map),
data => Err(MpvError::ValueContainsUnexpectedType {
expected_type: "Map<String, Value>".to_owned(),
received: data.clone(),
}),
})
.enumerate()
.map(|(id, entry)| {
entry.map(|mut entry| {
entry.id = id;
entry
})
})
.collect()
output.push(PlaylistEntry {
id,
filename,
title,
current,
});
}
output
}
#[cfg(test)]
@ -313,7 +277,7 @@ mod test {
}
#[test]
fn test_json_array_to_playlist() -> Result<(), MpvError> {
fn test_json_array_to_playlist() {
let json = json!([
{
"filename": "file1",
@ -324,10 +288,6 @@ mod test {
"filename": "file2",
"title": "title2",
"current": false
},
{
"filename": "file3",
"current": false
}
]);
@ -335,25 +295,17 @@ mod test {
PlaylistEntry {
id: 0,
filename: "file1".to_string(),
title: Some("title1".to_string()),
title: "title1".to_string(),
current: true,
},
PlaylistEntry {
id: 1,
filename: "file2".to_string(),
title: Some("title2".to_string()),
current: false,
},
PlaylistEntry {
id: 2,
filename: "file3".to_string(),
title: None,
title: "title2".to_string(),
current: false,
},
];
assert_eq!(json_array_to_playlist(json.as_array().unwrap())?, expected);
Ok(())
assert_eq!(json_array_to_playlist(json.as_array().unwrap()), expected);
}
}

View File

@ -1,5 +1,5 @@
use futures::{stream::StreamExt, Stream};
use mpvipc_async::{parse_property, Event, Mpv, MpvError, MpvExt, Property};
use mpvipc::{parse_event_property, Event, Mpv, MpvError, MpvExt};
use thiserror::Error;
use tokio::time::sleep;
use tokio::time::{timeout, Duration};
@ -8,22 +8,17 @@ use test_log::test;
use super::*;
const MPV_CHANNEL_ID: u64 = 1337;
const MPV_CHANNEL_ID: usize = 1337;
#[derive(Error, Debug)]
enum PropertyCheckingThreadError {
#[error("Unexpected property: {0:?}")]
UnexpectedPropertyError(Property),
UnexpectedPropertyError(mpvipc::Property),
#[error(transparent)]
MpvError(#[from] MpvError),
}
/// This function will create an ongoing tokio task that collects [`Event::PropertyChange`] events,
/// and parses them into [`Property`]s. It will then run the property through the provided
/// closure, and return an error if the closure returns false.
///
/// The returned cancellation token can be used to stop the task.
fn create_interruptable_event_property_checking_thread<T>(
mut events: impl Stream<Item = Result<Event, MpvError>> + Unpin + Send + 'static,
on_property: T,
@ -32,7 +27,7 @@ fn create_interruptable_event_property_checking_thread<T>(
tokio_util::sync::CancellationToken,
)
where
T: Fn(Property) -> bool + Send + 'static,
T: Fn(mpvipc::Property) -> bool + Send + 'static,
{
let cancellation_token = tokio_util::sync::CancellationToken::new();
let cancellation_token_clone = cancellation_token.clone();
@ -43,8 +38,8 @@ where
match event {
Some(Ok(event)) => {
match event {
Event::PropertyChange { id: Some(MPV_CHANNEL_ID), name, data } => {
let property = parse_property(&name, data).unwrap();
Event::PropertyChange { id: MPV_CHANNEL_ID, .. } => {
let property = parse_event_property(event).unwrap().1;
if !on_property(property.clone()) {
return Err(PropertyCheckingThreadError::UnexpectedPropertyError(property))
}
@ -66,9 +61,6 @@ where
(handle, cancellation_token)
}
/// This helper function will gracefully shut down both the event checking thread and the mpv process.
/// It will also return an error if the event checking thread happened to panic, or if it times out
/// The timeout is hardcoded to 500ms.
async fn graceful_shutdown(
cancellation_token: tokio_util::sync::CancellationToken,
handle: tokio::task::JoinHandle<Result<(), PropertyCheckingThreadError>>,
@ -111,7 +103,6 @@ async fn graceful_shutdown(
Ok(())
}
/// Test correct parsing of different values of the "pause" property
#[test(tokio::test)]
#[cfg(target_family = "unix")]
async fn test_highlevel_event_pause() -> Result<(), MpvError> {
@ -122,7 +113,7 @@ async fn test_highlevel_event_pause() -> Result<(), MpvError> {
let events = mpv.get_event_stream().await;
let (handle, cancellation_token) =
create_interruptable_event_property_checking_thread(events, |property| match property {
Property::Pause(_) => {
mpvipc::Property::Pause(_) => {
log::debug!("{:?}", property);
true
}
@ -140,7 +131,6 @@ async fn test_highlevel_event_pause() -> Result<(), MpvError> {
Ok(())
}
/// Test correct parsing of different values of the "volume" property
#[test(tokio::test)]
#[cfg(target_family = "unix")]
async fn test_highlevel_event_volume() -> Result<(), MpvError> {
@ -150,7 +140,7 @@ async fn test_highlevel_event_volume() -> Result<(), MpvError> {
let events = mpv.get_event_stream().await;
let (handle, cancellation_token) =
create_interruptable_event_property_checking_thread(events, |property| match property {
Property::Volume(_) => {
mpvipc::Property::Volume(_) => {
log::trace!("{:?}", property);
true
}
@ -170,7 +160,6 @@ async fn test_highlevel_event_volume() -> Result<(), MpvError> {
Ok(())
}
/// Test correct parsing of different values of the "mute" property
#[test(tokio::test)]
#[cfg(target_family = "unix")]
async fn test_highlevel_event_mute() -> Result<(), MpvError> {
@ -180,7 +169,7 @@ async fn test_highlevel_event_mute() -> Result<(), MpvError> {
let events = mpv.get_event_stream().await;
let (handle, cancellation_token) =
create_interruptable_event_property_checking_thread(events, |property| match property {
Property::Mute(_) => {
mpvipc::Property::Mute(_) => {
log::trace!("{:?}", property);
true
}
@ -198,7 +187,6 @@ async fn test_highlevel_event_mute() -> Result<(), MpvError> {
Ok(())
}
/// Test correct parsing of different values of the "duration" property
#[test(tokio::test)]
#[cfg(target_family = "unix")]
async fn test_highlevel_event_duration() -> Result<(), MpvError> {
@ -209,7 +197,7 @@ async fn test_highlevel_event_duration() -> Result<(), MpvError> {
let events = mpv.get_event_stream().await;
let (handle, cancellation_token) =
create_interruptable_event_property_checking_thread(events, |property| match property {
Property::Duration(_) => {
mpvipc::Property::Duration(_) => {
log::trace!("{:?}", property);
true
}

View File

@ -1,62 +1,26 @@
use mpvipc_async::{MpvError, MpvExt};
use mpvipc::MpvExt;
use super::*;
#[tokio::test]
#[cfg(target_family = "unix")]
async fn test_get_mpv_version() -> Result<(), MpvError> {
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();
let version: String = mpv.get_property("mpv-version").await.unwrap();
assert!(version.starts_with("mpv"));
mpv.kill().await.unwrap();
proc.kill().await.unwrap();
Ok(())
}
#[tokio::test]
#[cfg(target_family = "unix")]
async fn test_set_property() -> Result<(), MpvError> {
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();
let paused: bool = mpv.get_property("pause").await.unwrap();
assert!(paused);
mpv.kill().await.unwrap();
proc.kill().await.unwrap();
Ok(())
}
#[tokio::test]
#[cfg(target_family = "unix")]
async fn test_get_unavailable_property() -> Result<(), MpvError> {
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
let time_pos = mpv.get_property::<f64>("time-pos").await;
assert_eq!(time_pos, Ok(None));
mpv.kill().await.unwrap();
proc.kill().await.unwrap();
Ok(())
}
#[tokio::test]
#[cfg(target_family = "unix")]
async fn test_get_nonexistent_property() -> Result<(), MpvError> {
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
let nonexistent = mpv.get_property::<f64>("nonexistent").await;
match nonexistent {
Err(MpvError::MpvError { message, .. }) => {
assert_eq!(message, "property not found");
}
_ => panic!("Unexpected result: {:?}", nonexistent),
}
mpv.kill().await.unwrap();
proc.kill().await.unwrap();
Ok(())
}

View File

@ -1,6 +1,6 @@
use std::{path::Path, time::Duration};
use mpvipc_async::{Mpv, MpvError};
use mpvipc::{Mpv, MpvError};
use tokio::{
process::{Child, Command},
time::{sleep, timeout},
@ -25,7 +25,7 @@ pub async fn spawn_headless_mpv() -> Result<(Child, Mpv), MpvError> {
.spawn()
.expect("Failed to start mpv");
timeout(Duration::from_millis(1000), async {
timeout(Duration::from_millis(500), async {
while !&socket_path.exists() {
sleep(Duration::from_millis(10)).await;
}

View File

@ -1,5 +1,7 @@
use std::panic;
use futures::{stream::StreamExt, SinkExt};
use mpvipc_async::{Event, Mpv, MpvDataType, MpvExt};
use mpvipc::{parse_event_property, Mpv, MpvDataType, MpvExt, Property};
use serde_json::json;
use test_log::test;
use tokio::{net::UnixStream, task::JoinHandle};
@ -49,14 +51,19 @@ async fn test_observe_event_successful() {
tokio::spawn(async move {
let event = mpv2.get_event_stream().await.next().await.unwrap().unwrap();
assert_eq!(
event,
Event::PropertyChange {
id: Some(1),
name: "volume".to_string(),
data: Some(MpvDataType::Double(64.0))
let data = match parse_event_property(event) {
Ok((_, Property::Unknown { name, data })) => {
assert_eq!(name, "volume");
data
}
)
Ok((_, property)) => panic!("{:?}", property),
Err(err) => panic!("{:?}", err),
};
match data {
Some(MpvDataType::Double(data)) => assert_eq!(data, 64.0),
Some(data) => panic!("Unexpected value: {:?}", data),
None => panic!("No data"),
}
});
mpv.set_property("volume", 64.0).await.unwrap();

View File

@ -1,7 +1,7 @@
use std::{panic, time::Duration};
use futures::{stream::FuturesUnordered, SinkExt, StreamExt};
use mpvipc_async::{Mpv, MpvError, MpvExt, Playlist, PlaylistEntry};
use mpvipc::{Mpv, MpvError, MpvExt, Playlist, PlaylistEntry};
use serde_json::{json, Value};
use test_log::test;
use tokio::{net::UnixStream, task::JoinHandle};
@ -22,74 +22,73 @@ fn test_socket(answers: Vec<String>) -> (UnixStream, JoinHandle<Result<(), Lines
}
#[test(tokio::test)]
async fn test_get_property_successful() -> Result<(), MpvError> {
async fn test_get_property_successful() {
let (server, join_handle) = test_socket(vec![
json!({ "data": 100.0, "request_id": 0, "error": "success" }).to_string(),
]);
let mpv = Mpv::connect_socket(server).await?;
let volume: Option<f64> = mpv.get_property("volume").await?;
let mpv = Mpv::connect_socket(server).await.unwrap();
let volume: f64 = mpv.get_property("volume").await.unwrap();
assert_eq!(volume, Some(100.0));
assert_eq!(volume, 100.0);
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_get_property_broken_pipe() -> Result<(), MpvError> {
async fn test_get_property_broken_pipe() {
let (server, join_handle) = test_socket(vec![]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let maybe_volume = mpv.get_property::<f64>("volume").await;
assert_eq!(
maybe_volume,
Err(MpvError::MpvSocketConnectionError(
"Broken pipe (os error 32)".to_string()
))
);
match maybe_volume {
Err(MpvError::MpvSocketConnectionError(err)) => {
assert_eq!(err.to_string(), "Broken pipe (os error 32)");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_get_property_wrong_type() -> Result<(), MpvError> {
async fn test_get_property_wrong_type() {
let (server, join_handle) = test_socket(vec![
json!({ "data": 100.0, "request_id": 0, "error": "success" }).to_string(),
]);
let mpv = Mpv::connect_socket(server).await?;
let mpv = Mpv::connect_socket(server).await.unwrap();
let maybe_volume = mpv.get_property::<bool>("volume").await;
assert_eq!(
maybe_volume,
match maybe_volume {
Err(MpvError::ValueContainsUnexpectedType {
expected_type: "bool".to_string(),
received: json!(100.0)
})
);
expected_type,
received,
}) => {
assert_eq!(expected_type, "bool");
assert_eq!(received, json!(100.0));
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_get_unavailable_property() -> Result<(), MpvError> {
async fn test_get_property_error() {
let (server, join_handle) = test_socket(vec![
json!({ "error": "property unavailable", "request_id": 0 }).to_string(),
]);
let mpv = Mpv::connect_socket(server).await?;
let mpv = Mpv::connect_socket(server).await.unwrap();
let maybe_volume = mpv.get_property::<f64>("volume").await;
assert_eq!(maybe_volume, Ok(None),);
match maybe_volume {
Err(MpvError::MpvError(err)) => {
assert_eq!(err, "property unavailable");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
@ -116,7 +115,7 @@ async fn test_get_property_simultaneous_requests() {
}
_ => {
let response =
json!({ "error": "property not found", "request_id": 0 }).to_string();
json!({ "error": "property unavailable", "request_id": 0 }).to_string();
framed.send(response).await.unwrap();
}
}
@ -131,8 +130,8 @@ async fn test_get_property_simultaneous_requests() {
let mpv_clone_1 = mpv.clone();
let mpv_poller_1 = tokio::spawn(async move {
loop {
let volume: Option<f64> = mpv_clone_1.get_property("volume").await.unwrap();
assert_eq!(volume, Some(100.0));
let volume: f64 = mpv_clone_1.get_property("volume").await.unwrap();
assert_eq!(volume, 100.0);
}
});
@ -140,8 +139,8 @@ async fn test_get_property_simultaneous_requests() {
let mpv_poller_2 = tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_millis(1)).await;
let paused: Option<bool> = mpv_clone_2.get_property("pause").await.unwrap();
assert_eq!(paused, Some(true));
let paused: bool = mpv_clone_2.get_property("pause").await.unwrap();
assert!(paused);
}
});
@ -151,8 +150,8 @@ async fn test_get_property_simultaneous_requests() {
tokio::time::sleep(Duration::from_millis(2)).await;
let maybe_volume = mpv_clone_3.get_property::<f64>("nonexistent").await;
match maybe_volume {
Err(MpvError::MpvError { message, .. }) => {
assert_eq!(message, "property not found");
Err(MpvError::MpvError(err)) => {
assert_eq!(err, "property unavailable");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
@ -174,24 +173,24 @@ async fn test_get_property_simultaneous_requests() {
}
#[test(tokio::test)]
async fn test_get_playlist() -> Result<(), MpvError> {
async fn test_get_playlist() {
let expected = Playlist(vec![
PlaylistEntry {
id: 0,
filename: "file1".to_string(),
title: Some("title1".to_string()),
title: "title1".to_string(),
current: false,
},
PlaylistEntry {
id: 1,
filename: "file2".to_string(),
title: Some("title2".to_string()),
title: "title2".to_string(),
current: true,
},
PlaylistEntry {
id: 2,
filename: "file3".to_string(),
title: Some("title3".to_string()),
title: "title3".to_string(),
current: false,
},
]);
@ -209,26 +208,22 @@ async fn test_get_playlist() -> Result<(), MpvError> {
})
.to_string()]);
let mpv = Mpv::connect_socket(server).await?;
let playlist = mpv.get_playlist().await?;
let mpv = Mpv::connect_socket(server).await.unwrap();
let playlist = mpv.get_playlist().await.unwrap();
assert_eq!(playlist, expected);
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_get_playlist_empty() -> Result<(), MpvError> {
async fn test_get_playlist_empty() {
let (server, join_handle) = test_socket(vec![
json!({ "data": [], "request_id": 0, "error": "success" }).to_string(),
]);
let mpv = Mpv::connect_socket(server).await?;
let playlist = mpv.get_playlist().await?;
let mpv = Mpv::connect_socket(server).await.unwrap();
let playlist = mpv.get_playlist().await.unwrap();
assert_eq!(playlist, Playlist(vec![]));
join_handle.await.unwrap().unwrap();
Ok(())
}

View File

@ -1,7 +1,7 @@
use std::{panic, time::Duration};
use futures::{stream::FuturesUnordered, SinkExt, StreamExt};
use mpvipc_async::{Mpv, MpvError};
use mpvipc::{Mpv, MpvError, MpvExt, Playlist, PlaylistEntry};
use serde_json::{json, Value};
use test_log::test;
use tokio::{net::UnixStream, task::JoinHandle};
@ -22,78 +22,69 @@ fn test_socket(answers: Vec<String>) -> (UnixStream, JoinHandle<Result<(), Lines
}
#[test(tokio::test)]
async fn test_set_property_successful() -> Result<(), MpvError> {
async fn test_set_property_successful() {
let (server, join_handle) = test_socket(vec![
json!({ "data": null, "request_id": 0, "error": "success" }).to_string(),
]);
let mpv = Mpv::connect_socket(server).await?;
mpv.set_property("volume", 64.0).await?;
let mpv = Mpv::connect_socket(server).await.unwrap();
let volume = mpv.set_property("volume", 64.0).await;
assert!(volume.is_ok());
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_set_property_broken_pipe() -> Result<(), MpvError> {
async fn test_set_property_broken_pipe() {
let (server, join_handle) = test_socket(vec![]);
let mpv = Mpv::connect_socket(server).await?;
let mpv = Mpv::connect_socket(server).await.unwrap();
let maybe_set_volume = mpv.set_property("volume", 64.0).await;
assert_eq!(
maybe_set_volume,
Err(MpvError::MpvSocketConnectionError(
"Broken pipe (os error 32)".to_string()
))
);
match maybe_set_volume {
Err(MpvError::MpvSocketConnectionError(err)) => {
assert_eq!(err.to_string(), "Broken pipe (os error 32)");
}
_ => panic!("Unexpected result: {:?}", maybe_set_volume),
}
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_set_property_wrong_type() -> Result<(), MpvError> {
async fn test_set_property_wrong_type() {
let (server, join_handle) = test_socket(vec![
json!({"request_id":0,"error":"unsupported format for accessing property"}).to_string(),
]);
let mpv = Mpv::connect_socket(server).await?;
let mpv = Mpv::connect_socket(server).await.unwrap();
let maybe_volume = mpv.set_property::<bool>("volume", true).await;
match maybe_volume {
Err(MpvError::MpvError { message, .. }) => {
assert_eq!(message, "unsupported format for accessing property");
Err(MpvError::MpvError(err)) => {
assert_eq!(err, "unsupported format for accessing property");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_get_property_error() -> Result<(), MpvError> {
async fn test_get_property_error() {
let (server, join_handle) = test_socket(vec![
json!({"request_id":0,"error":"property not found"}).to_string(),
]);
let mpv = Mpv::connect_socket(server).await?;
let mpv = Mpv::connect_socket(server).await.unwrap();
let maybe_volume = mpv.set_property("nonexistent", true).await;
match maybe_volume {
Err(MpvError::MpvError { message, .. }) => {
assert_eq!(message, "property not found");
Err(MpvError::MpvError(err)) => {
assert_eq!(err, "property not found");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
@ -163,8 +154,8 @@ async fn test_set_property_simultaneous_requests() {
tokio::time::sleep(Duration::from_millis(2)).await;
let maybe_volume = mpv_clone_3.set_property("nonexistent", "a").await;
match maybe_volume {
Err(MpvError::MpvError { message, .. }) => {
assert_eq!(message, "property not found");
Err(MpvError::MpvError(err)) => {
assert_eq!(err, "property not found");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
@ -184,3 +175,59 @@ async fn test_set_property_simultaneous_requests() {
panic!("One of the pollers quit unexpectedly");
};
}
#[test(tokio::test)]
async fn test_get_playlist() {
let expected = Playlist(vec![
PlaylistEntry {
id: 0,
filename: "file1".to_string(),
title: "title1".to_string(),
current: false,
},
PlaylistEntry {
id: 1,
filename: "file2".to_string(),
title: "title2".to_string(),
current: true,
},
PlaylistEntry {
id: 2,
filename: "file3".to_string(),
title: "title3".to_string(),
current: false,
},
]);
let (server, join_handle) = test_socket(vec![json!({
"data": expected.0.iter().map(|entry| {
json!({
"filename": entry.filename,
"title": entry.title,
"current": entry.current
})
}).collect::<Vec<Value>>(),
"request_id": 0,
"error": "success"
})
.to_string()]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let playlist = mpv.get_playlist().await.unwrap();
assert_eq!(playlist, expected);
join_handle.await.unwrap().unwrap();
}
#[test(tokio::test)]
async fn test_get_playlist_empty() {
let (server, join_handle) = test_socket(vec![
json!({ "data": [], "request_id": 0, "error": "success" }).to_string(),
]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let playlist = mpv.get_playlist().await.unwrap();
assert_eq!(playlist, Playlist(vec![]));
join_handle.await.unwrap().unwrap();
}