Compare commits

...

30 Commits
master ... main

Author SHA1 Message Date
a6c6bf4388
Move project from Projects to Grzegorz
All checks were successful
Build and test / check (push) Successful in 59s
Build and test / docs (push) Successful in 1m16s
Build and test / test (push) Successful in 2m27s
Build and test / build (push) Successful in 57s
2025-01-06 16:31:12 +01:00
5a74dd0b02
highlevel_api: fix pause toggling
Some checks failed
Build and test / docs (push) Has been cancelled
Build and test / check (push) Successful in 1m9s
Build and test / build (push) Successful in 1m10s
Build and test / test (push) Successful in 2m33s
2024-12-15 17:37:48 +01:00
ee5aa30335
core: mark MpvDataType as serde(untagged)
Some checks are pending
Build and test / build (push) Waiting to run
Build and test / check (push) Waiting to run
Build and test / test (push) Waiting to run
Build and test / docs (push) Waiting to run
2024-12-15 16:21:25 +01:00
e3297bef15
event_parser: make id optional
Some checks are pending
Build and test / build (push) Waiting to run
Build and test / check (push) Waiting to run
Build and test / test (push) Waiting to run
Build and test / docs (push) Waiting to run
2024-12-15 15:31:01 +01:00
00cae63272
.gitea/build-and-test: remove caching step
Some checks failed
Build and test / build (push) Successful in 1m1s
Build and test / check (push) Successful in 1m4s
Build and test / test (push) Failing after 2m18s
Build and test / docs (push) Successful in 2m55s
This don't seem to be working properly, and it takes a lot of time to
time out. Let's remove it for now
2024-12-14 14:05:23 +01:00
99884b670d
property_parser: fix typo in docstring
All checks were successful
Build and test / build (push) Successful in 10m30s
Build and test / check (push) Successful in 10m36s
Build and test / test (push) Successful in 2m20s
Build and test / docs (push) Successful in 12m5s
2024-12-14 13:52:58 +01:00
3fe7417d4c
core: add docstrings for variants of MpvCommand 2024-12-14 13:52:57 +01:00
eb7277e4fd
treewide: fix type for property change event ids 2024-12-14 13:51:12 +01:00
81479d2f64
MpvError: add copy of command for better context
All checks were successful
Build and test / test (push) Successful in 2m13s
Build and test / build (push) Successful in 10m26s
Build and test / docs (push) Successful in 11m19s
Build and test / check (push) Successful in 10m27s
2024-12-12 16:36:42 +01:00
b2a22a9a57
.gitea/build-and-test: enable cache
All checks were successful
Build and test / build (push) Successful in 10m31s
Build and test / docs (push) Successful in 10m41s
Build and test / check (push) Successful in 10m39s
Build and test / test (push) Successful in 2m17s
2024-12-12 14:47:55 +01:00
ac863c571e
core_api: add Default for Playlist 2024-12-12 14:47:55 +01:00
13397a59f7
tests/integration: increase mpv command timeout 2024-12-12 14:47:55 +01:00
be5c37b433
.gitea/build-and-test: limit test threads 2024-12-12 14:47:55 +01:00
3ca3d7784c
Move repo to Projects, some pipeline updates 2024-12-12 14:47:55 +01:00
fa937567bd
.gitea/build-and-test: switch over to the default builders
All checks were successful
Build and test / build (push) Successful in 10m39s
Build and test / docs (push) Successful in 11m13s
Build and test / check (push) Successful in 10m38s
Build and test / test (push) Successful in 16m13s
2024-12-09 23:59:59 +01:00
c129e5104d
.gitea/workflows: adjust rsync action url
All checks were successful
Build and test / build (push) Successful in 1m54s
Build and test / check (push) Successful in 1m56s
Build and test / test (push) Successful in 3m25s
Build and test / docs (push) Successful in 2m26s
2024-08-21 07:19:24 +02:00
467cac3c50
fix inverted switch for MpvExt::set_playback
All checks were successful
Build and test / build (push) Successful in 1m56s
Build and test / check (push) Successful in 1m55s
Build and test / docs (push) Successful in 3m57s
Build and test / test (push) Successful in 3m57s
2024-08-03 21:02:46 +02:00
6a0a275f64
cargo fmt + clippy
All checks were successful
Build and test / build (push) Successful in 1m58s
Build and test / check (push) Successful in 1m58s
Build and test / docs (push) Successful in 2m16s
Build and test / test (push) Successful in 3m27s
2024-08-03 17:07:52 +02:00
99e4622ef7
switch default branch from master to main 2024-08-03 17:07:50 +02:00
ee0c51eeba
Cargo.toml: use real name for co-author 2024-08-03 17:07:13 +02:00
961da5b301
rename project from mpvipc to mpvipc-async 2024-08-03 17:07:09 +02:00
cbd8b1aed2
Add TODO for missing async response events 2024-08-03 16:02:17 +02:00
9e0d8c0117
README: small fixups, add fork notice 2024-08-03 15:52:41 +02:00
bb050d7a6d
Add comments for integration_tests::event_property_parser 2024-08-03 15:38:49 +02:00
93366593c7
Allow a few more highlevel types to be nullable 2024-08-03 15:38:46 +02:00
4cc824d164
.envrc: init 2024-08-02 18:20:31 +02:00
d482e9eaf6
flake: fix RUST_SRC_PATH, add x86_64-darwin to systems 2024-05-05 14:06:21 +02:00
eb81d7c463
reuse property_parser for highlevel api, add more highlevel functions 2024-05-04 23:01:17 +02:00
66d54a58aa
add cargo-nextest to flake.nix devshell 2024-05-04 18:45:33 +02:00
650507e680
add support for parsing playlists as events + more
This brings several changes with it:

- `Mpv::get_property` now returns `Option`s in case `data` is
  nonexistent. There could be cases where this is different from
  `MpvDataType::Null` and `MpvDataType::MinusOne`.

- `MpvError` now implements `PartialEq`
2024-05-04 18:45:32 +02:00
22 changed files with 924 additions and 570 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

View File

@ -2,42 +2,31 @@ name: "Build and test"
on:
push:
branches:
- master
- main
pull_request:
jobs:
build:
runs-on: ubuntu-latest-personal
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build
run: cargo build --all-features --verbose --release
check:
runs-on: ubuntu-latest-personal
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: nightly
override: true
components: rustfmt, clippy
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Check code format
run: cargo fmt -- --check
@ -45,30 +34,27 @@ jobs:
run: cargo clippy --all-features -- --deny warnings
test:
runs-on: ubuntu-latest-personal
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: cargo-bins/cargo-binstall@main
- uses: actions/checkout@v4
- name: Install cargo binstall
uses: cargo-bins/cargo-binstall@main
- name: Install mpv
run: apt-get update && apt-get install -y mpv
- name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@nightly
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
cargo nextest run --all-features --release --no-fail-fast --test-threads=1
env:
RUST_LOG: "trace"
RUSTFLAGS: "-Cinstrument-coverage"
@ -96,38 +82,32 @@ jobs:
target/coverage/
- name: Upload test report
uses: https://git.pvv.ntnu.no/oysteikt/rsync-action@main
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
with:
source: target/coverage/html/
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="
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"
docs:
runs-on: ubuntu-latest-personal
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install latest nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build docs
run: cargo doc --all-features --document-private-items --release
- name: Transfer files
uses: https://git.pvv.ntnu.no/oysteikt/rsync-action@main
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
with:
source: target/doc/
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="
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"

View File

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

View File

@ -1,16 +1,18 @@
[![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/)
[![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).
# mpvipc
A small library which provides bindings to control existing mpv instances through sockets.
## Dependencies
- `mpv`
- `cargo` (make dependency)
- `cargo-nextest` (test depencency)
- `grcov` (test depencency)
- `mpv` (runtime dependency)
- `cargo-nextest` (optional test depencency)
- `grcov` (optional test depencency)
## Example
@ -23,12 +25,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::*;
use mpvipc_async::*;
#[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).expect("Error pausing");
mpv.set_property("pause", !paused).await.expect("Error pausing");
}
```
```

View File

@ -1,4 +1,4 @@
use mpvipc::{Mpv, MpvError, MpvExt};
use mpvipc_async::{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: f64 = mpv.get_property("playback-time").await?;
println!("playback-time: {}", playback_time);
let playback_time: Option<f64> = mpv.get_property("playback-time").await?;
println!("playback-time: {:?}", playback_time);
Ok(())
}

View File

@ -1,5 +1,5 @@
use futures::StreamExt;
use mpvipc::{parse_event_property, Event, Mpv, MpvDataType, MpvError, MpvExt, Property};
use mpvipc_async::{parse_property, Event, Mpv, MpvDataType, MpvError, MpvExt, Property};
fn seconds_to_hms(total: f64) -> String {
let total = total as u64;
@ -25,34 +25,36 @@ async fn main() -> Result<(), MpvError> {
let mut events = mpv.get_event_stream().await;
while let Some(Ok(event)) = events.next().await {
match event {
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);
mpvipc_async::Event::PropertyChange { name, data, .. } => {
match parse_property(&name, data)? {
Property::Path(Some(value)) => println!("\nPlaying: {}", value),
Property::Pause(value) => {
println!("Pause: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("ALBUM") {
println!(" Album: {}", value);
Property::PlaybackTime(Some(value)) => {
println!("Playback time: {}", seconds_to_hms(value));
}
if let Some(MpvDataType::String(value)) = value.get("TITLE") {
println!(" Title: {}", value);
Property::Duration(Some(value)) => {
println!("Duration: {}", seconds_to_hms(value));
}
if let Some(MpvDataType::String(value)) = value.get("TRACK") {
println!(" Track: {}", 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);
}
}
_ => (),
}
_ => (),
},
}
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,8 +30,9 @@
])
pkgs.mpv
pkgs.grcov
pkgs.cargo-nextest
];
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/";
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library";
});
};
}

View File

@ -27,39 +27,65 @@ 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,
PlaylistMove {
from: usize,
to: usize,
},
Observe {
id: usize,
property: String,
},
///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.
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>),
ScriptMessageTo {
target: String,
args: Vec<String>,
},
Seek {
seconds: f64,
option: SeekOptions,
},
/// 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.
Stop,
Unobserve(usize),
/// Unobserve all properties registered with the given id.
/// See [`MpvCommand::Observe`] for more context.
Unobserve(u64),
}
/// Helper trait to keep track of the string literals that mpv expects.
@ -69,6 +95,7 @@ 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),
@ -82,7 +109,7 @@ pub enum MpvDataType {
}
/// A mpv playlist.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Playlist(pub Vec<PlaylistEntry>);
/// A single entry in the mpv playlist.
@ -90,7 +117,7 @@ pub struct Playlist(pub Vec<PlaylistEntry>);
pub struct PlaylistEntry {
pub id: usize,
pub filename: String,
pub title: String,
pub title: Option<String>,
pub current: bool,
}
@ -134,18 +161,22 @@ 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<Self, MpvError>;
async fn get_property_generic(instance: &Mpv, property: &str)
-> Result<Option<Self>, MpvError>;
}
impl<T> GetPropertyTypeHandler for T
where
T: TypeHandler,
{
async fn get_property_generic(instance: &Mpv, property: &str) -> Result<T, MpvError> {
async fn get_property_generic(instance: &Mpv, property: &str) -> Result<Option<T>, MpvError> {
instance
.get_property_value(property)
.await
.and_then(T::get_value)
.and_then(|value| match value {
Some(v) => T::get_value(v).map(|v| Some(v)),
None => Ok(None),
})
}
}
@ -172,7 +203,7 @@ where
instance
.command_sender
.send((
MpvIpcCommand::SetProperty(property.to_owned(), value),
MpvIpcCommand::SetProperty(property.to_owned(), value.to_owned()),
res_tx,
))
.await
@ -277,7 +308,7 @@ impl Mpv {
command: &str,
args: &[&str],
) -> Result<Option<Value>, MpvError> {
let command = Vec::from(
let command_vec = Vec::from(
[command]
.iter()
.chain(args.iter())
@ -287,7 +318,7 @@ impl Mpv {
);
let (res_tx, res_rx) = oneshot::channel();
self.command_sender
.send((MpvIpcCommand::Command(command), res_tx))
.send((MpvIpcCommand::Command(command_vec.clone()), res_tx))
.await
.map_err(|err| MpvError::InternalConnectionError(err.to_string()))?;
@ -317,7 +348,7 @@ impl Mpv {
///
/// # Example
/// ```
/// use mpvipc::{Mpv, MpvError};
/// use mpvipc_async::{Mpv, MpvError};
///
/// #[tokio::main]
/// async fn main() -> Result<(), MpvError> {
@ -449,7 +480,7 @@ impl Mpv {
///
/// # Example
/// ```
/// use mpvipc::{Mpv, MpvError};
/// use mpvipc_async::{Mpv, MpvError};
///
/// #[tokio::main]
/// async fn main() -> Result<(), MpvError> {
@ -462,7 +493,7 @@ impl Mpv {
pub async fn get_property<T: GetPropertyTypeHandler>(
&self,
property: &str,
) -> Result<T, MpvError> {
) -> Result<Option<T>, MpvError> {
T::get_property_generic(self, property).await
}
@ -478,7 +509,7 @@ impl Mpv {
/// # Example
///
/// ```
/// use mpvipc::{Mpv, MpvError};
/// use mpvipc_async::{Mpv, MpvError};
///
/// #[tokio::main]
/// async fn main() -> Result<(), MpvError> {
@ -487,7 +518,7 @@ impl Mpv {
/// Ok(())
/// }
/// ```
pub async fn get_property_value(&self, property: &str) -> Result<Value, MpvError> {
pub async fn get_property_value(&self, property: &str) -> Result<Option<Value>, MpvError> {
let (res_tx, res_rx) = oneshot::channel();
self.command_sender
.send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx))
@ -495,9 +526,7 @@ impl Mpv {
.map_err(|err| MpvError::InternalConnectionError(err.to_string()))?;
match res_rx.await {
Ok(MpvIpcResponse(response)) => {
response.and_then(|value| value.ok_or(MpvError::MissingMpvData))
}
Ok(MpvIpcResponse(response)) => response,
Err(err) => Err(MpvError::InternalConnectionError(err.to_string())),
}
}
@ -519,18 +548,17 @@ impl Mpv {
///
/// # Example
/// ```
/// use mpvipc::{Mpv, MpvError};
/// use mpvipc_async::{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: SetPropertyTypeHandler<T>>(
&self,
property: &str,
value: T,
) -> Result<(), MpvError> {
T::set_property_generic(self, property, value).await
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
}
}

View File

@ -3,13 +3,16 @@
use serde_json::{Map, Value};
use thiserror::Error;
use crate::MpvDataType;
use crate::{MpvDataType, Property};
/// Any error that can occur when interacting with mpv.
#[derive(Error, Debug)]
pub enum MpvError {
#[error("MpvError: {0}")]
MpvError(String),
#[error("Mpv returned error in response to command: {message}\nCommand: {command:#?}")]
MpvError {
command: Vec<Value>,
message: String,
},
#[error("Error communicating over mpv socket: {0}")]
MpvSocketConnectionError(String),
@ -43,6 +46,63 @@ 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: usize,
id: Option<u64>,
name: String,
data: Option<MpvDataType>,
},
@ -209,6 +209,12 @@ 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" =>
@ -290,7 +296,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_key_as!(as_u64, "id", event) as usize;
let id = get_optional_key_as!(as_u64, "id", event);
let property_name = get_key_as!(as_str, "name", event);
let data = event.get("data").map(json_to_value).transpose()?;

View File

@ -1,8 +1,8 @@
//! High-level API extension for [`Mpv`].
use crate::{
IntoRawCommandPart, Mpv, MpvCommand, MpvDataType, MpvError, Playlist, PlaylistAddOptions,
PlaylistEntry, SeekOptions,
parse_property, IntoRawCommandPart, LoopProperty, Mpv, MpvCommand, MpvDataType, MpvError,
Playlist, PlaylistAddOptions, Property, SeekOptions,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@ -44,35 +44,7 @@ pub enum PlaylistAddTypeOptions {
// TODO: fix this
#[allow(async_fn_in_trait)]
pub trait MpvExt {
/// 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>;
// COMMANDS
/// Seek to a specific position in the current video.
async fn seek(&self, seconds: f64, option: SeekOptions) -> Result<(), MpvError>;
@ -116,11 +88,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: usize, property: &str) -> Result<(), MpvError>;
async fn observe_property(&self, id: u64, 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: usize) -> Result<(), MpvError>;
async fn unobserve_property(&self, id: u64) -> Result<(), MpvError>;
/// Skip to the next entry in the playlist.
async fn next(&self) -> Result<(), MpvError>;
@ -132,71 +104,122 @@ 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 {
async fn get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError> {
self.get_property("metadata").await
// COMMANDS
async fn seek(&self, seconds: f64, option: SeekOptions) -> Result<(), MpvError> {
self.run_command(MpvCommand::Seek { seconds, option }).await
}
async fn get_playlist(&self) -> Result<Playlist, MpvError> {
self.get_property::<Vec<PlaylistEntry>>("playlist")
async fn playlist_shuffle(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistShuffle).await
}
async fn playlist_remove_id(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistRemove(id)).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)),
}?;
self.run_command(MpvCommand::PlaylistMove {
from: id,
to: current_id + 1,
})
.await
}
async fn playlist_play_id(&self, id: usize) -> Result<(), MpvError> {
self.set_property("playlist-pos", id).await
}
async fn playlist_move_id(&self, from: usize, to: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistMove { from, to })
.await
.map(Playlist)
}
async fn kill(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Quit).await
}
async fn next(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistNext).await
}
async fn observe_property(&self, id: usize, property: &str) -> Result<(), MpvError> {
self.run_command(MpvCommand::Observe {
id,
property: property.to_string(),
})
.await
}
async fn unobserve_property(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::Unobserve(id)).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 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_clear(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistClear).await
}
async fn playlist_add(
@ -224,138 +247,237 @@ impl MpvExt for Mpv {
}
}
async fn playlist_clear(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistClear).await
async fn restart(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Seek {
seconds: 0f64,
option: SeekOptions::Absolute,
})
.await
}
async fn playlist_move_id(&self, from: usize, to: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistMove { from, to })
.await
async fn prev(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistPrev).await
}
async fn playlist_play_id(&self, id: usize) -> Result<(), MpvError> {
self.set_property("playlist-pos", id).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_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
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
}
Err(msg) => Err(msg),
NumberChangeOptions::Decrease => {
self.set_property("volume", volume - input_volume).await
}
NumberChangeOptions::Absolute => self.set_property("volume", input_volume).await,
}
}
async fn playlist_remove_id(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistRemove(id)).await
}
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 => "inf",
Switch::Off => "no",
Switch::Toggle => {
self.get_property::<String>("loop-file")
.await
.map(|s| match s.as_str() {
"inf" => "no",
"no" => "inf",
_ => "no",
})?
}
};
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> {
let enabled = match option {
Switch::On => "yes",
Switch::Off => "no",
Switch::Toggle => {
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_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
}
let speed = self.get_speed().await?;
NumberChangeOptions::Decrease => {
self.set_property("speed", speed - input_speed).await
}
NumberChangeOptions::Absolute => self.set_property("speed", input_speed).await,
},
Err(msg) => Err(msg),
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 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
async fn set_playback(&self, option: Switch) -> Result<(), MpvError> {
let enabled = match option {
Switch::On => "no",
Switch::Off => "yes",
Switch::Toggle => {
if self.is_playing().await? {
"yes"
} else {
"no"
}
}
};
self.set_property("pause", enabled).await
}
NumberChangeOptions::Decrease => {
self.set_property("volume", volume - input_volume).await
async fn set_mute(&self, option: Switch) -> Result<(), MpvError> {
let enabled = match option {
Switch::On => "yes",
Switch::Off => "no",
Switch::Toggle => {
if self.is_muted().await? {
"no"
} else {
"yes"
}
}
};
self.set_property("mute", enabled).await
}
NumberChangeOptions::Absolute => self.set_property("volume", input_volume).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",
},
Err(msg) => Err(msg),
};
self.set_property("loop-playlist", enabled).await
}
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",
},
};
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)),
}
}
async fn stop(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Stop).await
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)),
}
}
}

View File

@ -24,8 +24,8 @@ pub(crate) enum MpvIpcCommand {
Command(Vec<String>),
GetProperty(String),
SetProperty(String, Value),
ObserveProperty(usize, String),
UnobserveProperty(usize),
ObserveProperty(u64, String),
UnobserveProperty(u64),
Exit,
}
@ -92,7 +92,7 @@ impl MpvIpc {
log::trace!("Received response: {:?}", response);
parse_mpv_response_data(response?)
parse_mpv_response_data(response?, command)
}
pub(crate) async fn get_mpv_property(
@ -114,17 +114,14 @@ impl MpvIpc {
pub(crate) async fn observe_property(
&mut self,
id: usize,
id: u64,
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: usize,
) -> Result<Option<Value>, MpvError> {
pub(crate) async fn unobserve_property(&mut self, id: u64) -> Result<Option<Value>, MpvError> {
self.send_command(&[json!("unobserve_property"), json!(id)])
.await
}
@ -197,7 +194,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) -> Result<Option<Value>, MpvError> {
fn parse_mpv_response_data(value: Value, command: &[Value]) -> Result<Option<Value>, MpvError> {
log::trace!("Parsing mpv response data: {:?}", value);
let result = value
.as_object()
@ -224,7 +221,11 @@ fn parse_mpv_response_data(value: Value) -> Result<Option<Value>, MpvError> {
})
.and_then(|(error, data)| match error {
"success" => Ok(data),
err => Err(MpvError::MpvError(err.to_string())),
"property unavailable" => Ok(None),
err => Err(MpvError::MpvError {
command: command.to_owned(),
message: 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(),
})
.map(|array| json_array_to_playlist(array))
.and_then(|array| json_array_to_playlist(array))
}
fn as_string(&self) -> String {
@ -155,29 +155,65 @@ pub(crate) fn json_array_to_vec(array: &[Value]) -> Result<Vec<MpvDataType>, Mpv
array.iter().map(json_to_value).collect()
}
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();
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(),
})
}
if let Value::String(ref t) = entry["title"] {
title = t.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::Bool(ref b) = entry["current"] {
current = *b;
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(),
})
}
output.push(PlaylistEntry {
id,
filename,
title,
current,
});
}
output
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()
}
#[cfg(test)]
@ -277,7 +313,7 @@ mod test {
}
#[test]
fn test_json_array_to_playlist() {
fn test_json_array_to_playlist() -> Result<(), MpvError> {
let json = json!([
{
"filename": "file1",
@ -288,6 +324,10 @@ mod test {
"filename": "file2",
"title": "title2",
"current": false
},
{
"filename": "file3",
"current": false
}
]);
@ -295,17 +335,25 @@ mod test {
PlaylistEntry {
id: 0,
filename: "file1".to_string(),
title: "title1".to_string(),
title: Some("title1".to_string()),
current: true,
},
PlaylistEntry {
id: 1,
filename: "file2".to_string(),
title: "title2".to_string(),
title: Some("title2".to_string()),
current: false,
},
PlaylistEntry {
id: 2,
filename: "file3".to_string(),
title: None,
current: false,
},
];
assert_eq!(json_array_to_playlist(json.as_array().unwrap()), expected);
assert_eq!(json_array_to_playlist(json.as_array().unwrap())?, expected);
Ok(())
}
}

View File

@ -1,21 +1,24 @@
//! JSON parsing logic for properties returned in [`Event::PropertyChange`]
//! JSON parsing logic for properties returned by
//! [`Event::PropertyChange`], and used internally in `MpvExt`
//! to parse the response from `Mpv::get_property()`.
//!
//! This module is used to parse the json data from the `data` field of the
//! [`Event::PropertyChange`] variant. Mpv has about 1000 different properties
//! This module is used to parse the json data from the `data` field of
//! known properties. 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::{Event, MpvDataType, MpvError, PlaylistEntry};
use crate::{MpvDataType, MpvError, PlaylistEntry};
/// All possible properties that can be observed through the event system.
/// An incomplete list of properties that mpv can return.
///
/// 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.
/// Unimplemented properties will be returned with it's data
/// as a `Property::Unknown` variant.
///
/// See <https://mpv.io/manual/master/#properties> for
/// the upstream list of properties.
@ -31,9 +34,12 @@ 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>,
@ -48,17 +54,12 @@ pub enum LoopProperty {
No,
}
/// 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() {
/// 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 {
"path" => {
let path = match data {
Some(MpvDataType::String(s)) => Some(s),
@ -73,7 +74,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
return Err(MpvError::MissingMpvData);
}
};
Ok((id, Property::Path(path)))
Ok(Property::Path(path))
}
"pause" => {
let pause = match data {
@ -88,7 +89,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
return Err(MpvError::MissingMpvData);
}
};
Ok((id, Property::Pause(pause)))
Ok(Property::Pause(pause))
}
"playback-time" => {
let playback_time = match data {
@ -101,7 +102,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
})
}
};
Ok((id, Property::PlaybackTime(playback_time)))
Ok(Property::PlaybackTime(playback_time))
}
"duration" => {
let duration = match data {
@ -114,7 +115,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
})
}
};
Ok((id, Property::Duration(duration)))
Ok(Property::Duration(duration))
}
"metadata" => {
let metadata = match data {
@ -127,16 +128,21 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
})
}
};
Ok((id, Property::Metadata(metadata)))
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))
}
// "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),
@ -150,7 +156,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
})
}
};
Ok((id, Property::PlaylistPos(playlist_pos)))
Ok(Property::PlaylistPos(playlist_pos))
}
"loop-file" => {
let loop_file = match data.to_owned() {
@ -172,7 +178,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
},
None => MpvError::MissingMpvData,
})?;
Ok((id, Property::LoopFile(loop_file)))
Ok(Property::LoopFile(loop_file))
}
"loop-playlist" => {
let loop_playlist = match data.to_owned() {
@ -195,7 +201,34 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
None => MpvError::MissingMpvData,
})?;
Ok((id, Property::LoopPlaylist(loop_playlist)))
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))
}
"speed" => {
let speed = match data {
@ -210,7 +243,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
return Err(MpvError::MissingMpvData);
}
};
Ok((id, Property::Speed(speed)))
Ok(Property::Speed(speed))
}
"volume" => {
let volume = match data {
@ -225,7 +258,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
return Err(MpvError::MissingMpvData);
}
};
Ok((id, Property::Volume(volume)))
Ok(Property::Volume(volume))
}
"mute" => {
let mute = match data {
@ -240,9 +273,81 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
return Err(MpvError::MissingMpvData);
}
};
Ok((id, Property::Mute(mute)))
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))
}
// TODO: add missing cases
_ => Ok((id, Property::Unknown { name, data })),
_ => Ok(Property::Unknown {
name: name.to_owned(),
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()
}

Binary file not shown.

View File

@ -1,5 +1,5 @@
use futures::{stream::StreamExt, Stream};
use mpvipc::{parse_event_property, Event, Mpv, MpvError, MpvExt};
use mpvipc_async::{parse_property, Event, Mpv, MpvError, MpvExt, Property};
use thiserror::Error;
use tokio::time::sleep;
use tokio::time::{timeout, Duration};
@ -8,17 +8,22 @@ use test_log::test;
use super::*;
const MPV_CHANNEL_ID: usize = 1337;
const MPV_CHANNEL_ID: u64 = 1337;
#[derive(Error, Debug)]
enum PropertyCheckingThreadError {
#[error("Unexpected property: {0:?}")]
UnexpectedPropertyError(mpvipc::Property),
UnexpectedPropertyError(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,
@ -27,7 +32,7 @@ fn create_interruptable_event_property_checking_thread<T>(
tokio_util::sync::CancellationToken,
)
where
T: Fn(mpvipc::Property) -> bool + Send + 'static,
T: Fn(Property) -> bool + Send + 'static,
{
let cancellation_token = tokio_util::sync::CancellationToken::new();
let cancellation_token_clone = cancellation_token.clone();
@ -38,8 +43,8 @@ where
match event {
Some(Ok(event)) => {
match event {
Event::PropertyChange { id: MPV_CHANNEL_ID, .. } => {
let property = parse_event_property(event).unwrap().1;
Event::PropertyChange { id: Some(MPV_CHANNEL_ID), name, data } => {
let property = parse_property(&name, data).unwrap();
if !on_property(property.clone()) {
return Err(PropertyCheckingThreadError::UnexpectedPropertyError(property))
}
@ -61,6 +66,9 @@ 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>>,
@ -103,6 +111,7 @@ 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> {
@ -113,7 +122,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 {
mpvipc::Property::Pause(_) => {
Property::Pause(_) => {
log::debug!("{:?}", property);
true
}
@ -131,6 +140,7 @@ 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> {
@ -140,7 +150,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 {
mpvipc::Property::Volume(_) => {
Property::Volume(_) => {
log::trace!("{:?}", property);
true
}
@ -160,6 +170,7 @@ 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> {
@ -169,7 +180,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 {
mpvipc::Property::Mute(_) => {
Property::Mute(_) => {
log::trace!("{:?}", property);
true
}
@ -187,6 +198,7 @@ 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> {
@ -197,7 +209,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 {
mpvipc::Property::Duration(_) => {
Property::Duration(_) => {
log::trace!("{:?}", property);
true
}

View File

@ -1,26 +1,62 @@
use mpvipc::MpvExt;
use mpvipc_async::{MpvError, MpvExt};
use super::*;
#[tokio::test]
#[cfg(target_family = "unix")]
async fn test_get_mpv_version() {
async fn test_get_mpv_version() -> Result<(), MpvError> {
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() {
async fn test_set_property() -> Result<(), MpvError> {
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::{Mpv, MpvError};
use mpvipc_async::{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(500), async {
timeout(Duration::from_millis(1000), async {
while !&socket_path.exists() {
sleep(Duration::from_millis(10)).await;
}

View File

@ -1,7 +1,5 @@
use std::panic;
use futures::{stream::StreamExt, SinkExt};
use mpvipc::{parse_event_property, Mpv, MpvDataType, MpvExt, Property};
use mpvipc_async::{Event, Mpv, MpvDataType, MpvExt};
use serde_json::json;
use test_log::test;
use tokio::{net::UnixStream, task::JoinHandle};
@ -51,19 +49,14 @@ async fn test_observe_event_successful() {
tokio::spawn(async move {
let event = mpv2.get_event_stream().await.next().await.unwrap().unwrap();
let data = match parse_event_property(event) {
Ok((_, Property::Unknown { name, data })) => {
assert_eq!(name, "volume");
data
assert_eq!(
event,
Event::PropertyChange {
id: Some(1),
name: "volume".to_string(),
data: Some(MpvDataType::Double(64.0))
}
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::{Mpv, MpvError, MpvExt, Playlist, PlaylistEntry};
use mpvipc_async::{Mpv, MpvError, MpvExt, Playlist, PlaylistEntry};
use serde_json::{json, Value};
use test_log::test;
use tokio::{net::UnixStream, task::JoinHandle};
@ -22,73 +22,74 @@ fn test_socket(answers: Vec<String>) -> (UnixStream, JoinHandle<Result<(), Lines
}
#[test(tokio::test)]
async fn test_get_property_successful() {
async fn test_get_property_successful() -> Result<(), MpvError> {
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.unwrap();
let volume: f64 = mpv.get_property("volume").await.unwrap();
let mpv = Mpv::connect_socket(server).await?;
let volume: Option<f64> = mpv.get_property("volume").await?;
assert_eq!(volume, 100.0);
assert_eq!(volume, Some(100.0));
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_get_property_broken_pipe() {
async fn test_get_property_broken_pipe() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let maybe_volume = mpv.get_property::<f64>("volume").await;
match maybe_volume {
Err(MpvError::MpvSocketConnectionError(err)) => {
assert_eq!(err.to_string(), "Broken pipe (os error 32)");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
assert_eq!(
maybe_volume,
Err(MpvError::MpvSocketConnectionError(
"Broken pipe (os error 32)".to_string()
))
);
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_get_property_wrong_type() {
async fn test_get_property_wrong_type() -> Result<(), MpvError> {
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.unwrap();
let mpv = Mpv::connect_socket(server).await?;
let maybe_volume = mpv.get_property::<bool>("volume").await;
match maybe_volume {
assert_eq!(
maybe_volume,
Err(MpvError::ValueContainsUnexpectedType {
expected_type,
received,
}) => {
assert_eq!(expected_type, "bool");
assert_eq!(received, json!(100.0));
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
expected_type: "bool".to_string(),
received: json!(100.0)
})
);
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_get_property_error() {
async fn test_get_unavailable_property() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![
json!({ "error": "property unavailable", "request_id": 0 }).to_string(),
]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let mpv = Mpv::connect_socket(server).await?;
let maybe_volume = mpv.get_property::<f64>("volume").await;
match maybe_volume {
Err(MpvError::MpvError(err)) => {
assert_eq!(err, "property unavailable");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
assert_eq!(maybe_volume, Ok(None),);
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
@ -115,7 +116,7 @@ async fn test_get_property_simultaneous_requests() {
}
_ => {
let response =
json!({ "error": "property unavailable", "request_id": 0 }).to_string();
json!({ "error": "property not found", "request_id": 0 }).to_string();
framed.send(response).await.unwrap();
}
}
@ -130,8 +131,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: f64 = mpv_clone_1.get_property("volume").await.unwrap();
assert_eq!(volume, 100.0);
let volume: Option<f64> = mpv_clone_1.get_property("volume").await.unwrap();
assert_eq!(volume, Some(100.0));
}
});
@ -139,8 +140,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: bool = mpv_clone_2.get_property("pause").await.unwrap();
assert!(paused);
let paused: Option<bool> = mpv_clone_2.get_property("pause").await.unwrap();
assert_eq!(paused, Some(true));
}
});
@ -150,8 +151,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(err)) => {
assert_eq!(err, "property unavailable");
Err(MpvError::MpvError { message, .. }) => {
assert_eq!(message, "property not found");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
@ -173,24 +174,24 @@ async fn test_get_property_simultaneous_requests() {
}
#[test(tokio::test)]
async fn test_get_playlist() {
async fn test_get_playlist() -> Result<(), MpvError> {
let expected = Playlist(vec![
PlaylistEntry {
id: 0,
filename: "file1".to_string(),
title: "title1".to_string(),
title: Some("title1".to_string()),
current: false,
},
PlaylistEntry {
id: 1,
filename: "file2".to_string(),
title: "title2".to_string(),
title: Some("title2".to_string()),
current: true,
},
PlaylistEntry {
id: 2,
filename: "file3".to_string(),
title: "title3".to_string(),
title: Some("title3".to_string()),
current: false,
},
]);
@ -208,22 +209,26 @@ async fn test_get_playlist() {
})
.to_string()]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let playlist = mpv.get_playlist().await.unwrap();
let mpv = Mpv::connect_socket(server).await?;
let playlist = mpv.get_playlist().await?;
assert_eq!(playlist, expected);
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_get_playlist_empty() {
async fn test_get_playlist_empty() -> Result<(), MpvError> {
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();
let mpv = Mpv::connect_socket(server).await?;
let playlist = mpv.get_playlist().await?;
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::{Mpv, MpvError, MpvExt, Playlist, PlaylistEntry};
use mpvipc_async::{Mpv, MpvError};
use serde_json::{json, Value};
use test_log::test;
use tokio::{net::UnixStream, task::JoinHandle};
@ -22,69 +22,78 @@ fn test_socket(answers: Vec<String>) -> (UnixStream, JoinHandle<Result<(), Lines
}
#[test(tokio::test)]
async fn test_set_property_successful() {
async fn test_set_property_successful() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![
json!({ "data": null, "request_id": 0, "error": "success" }).to_string(),
]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let volume = mpv.set_property("volume", 64.0).await;
let mpv = Mpv::connect_socket(server).await?;
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() {
async fn test_set_property_broken_pipe() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let mpv = Mpv::connect_socket(server).await?;
let maybe_set_volume = mpv.set_property("volume", 64.0).await;
match maybe_set_volume {
Err(MpvError::MpvSocketConnectionError(err)) => {
assert_eq!(err.to_string(), "Broken pipe (os error 32)");
}
_ => panic!("Unexpected result: {:?}", maybe_set_volume),
}
assert_eq!(
maybe_set_volume,
Err(MpvError::MpvSocketConnectionError(
"Broken pipe (os error 32)".to_string()
))
);
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
async fn test_set_property_wrong_type() {
async fn test_set_property_wrong_type() -> Result<(), MpvError> {
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.unwrap();
let mpv = Mpv::connect_socket(server).await?;
let maybe_volume = mpv.set_property::<bool>("volume", true).await;
match maybe_volume {
Err(MpvError::MpvError(err)) => {
assert_eq!(err, "unsupported format for accessing property");
Err(MpvError::MpvError { message, .. }) => {
assert_eq!(message, "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() {
async fn test_get_property_error() -> Result<(), MpvError> {
let (server, join_handle) = test_socket(vec![
json!({"request_id":0,"error":"property not found"}).to_string(),
]);
let mpv = Mpv::connect_socket(server).await.unwrap();
let mpv = Mpv::connect_socket(server).await?;
let maybe_volume = mpv.set_property("nonexistent", true).await;
match maybe_volume {
Err(MpvError::MpvError(err)) => {
assert_eq!(err, "property not found");
Err(MpvError::MpvError { message, .. }) => {
assert_eq!(message, "property not found");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
join_handle.await.unwrap().unwrap();
Ok(())
}
#[test(tokio::test)]
@ -154,8 +163,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(err)) => {
assert_eq!(err, "property not found");
Err(MpvError::MpvError { message, .. }) => {
assert_eq!(message, "property not found");
}
_ => panic!("Unexpected result: {:?}", maybe_volume),
}
@ -175,59 +184,3 @@ 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();
}