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

View File

@ -1,15 +1,14 @@
[package] [package]
name = "mpvipc" name = "mpvipc-async"
version = "1.3.0" version = "0.1.0"
authors = [ authors = [
"Jonas Frei <freijon@pm.me>", "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." description = "A small library which provides bindings to control existing mpv instances through sockets."
license = "GPL-3.0" license = "GPL-3.0"
homepage = "https://git.pvv.ntnu.no/oysteikt/mpvipc" repository = "https://git.pvv.ntnu.no/Grzegorz/mpvipc-async"
repository = "https://git.pvv.ntnu.no/oysteikt/mpvipc" documentation = "https://pages.pvv.ntnu.no/Grzegorz/mpvipc-async/main/docs/mpvipc_async/"
documentation = "https://pvv.ntnu.no/~oysteikt/gitea/mpvipc/master/docs/mpvipc/"
edition = "2021" edition = "2021"
rust-version = "1.75" 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/) [![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://pvv.ntnu.no/~oysteikt/gitea/mpvipc/master/docs/mpvipc/) [![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. A small library which provides bindings to control existing mpv instances through sockets.
## Dependencies ## Dependencies
- `mpv` - `mpv` (runtime dependency)
- `cargo` (make dependency) - `cargo-nextest` (optional test depencency)
- `cargo-nextest` (test depencency) - `grcov` (optional test depencency)
- `grcov` (test depencency)
## Example ## 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. Here is a small code example which connects to the socket `/tmp/mpv.sock` and toggles playback.
```rust ```rust
use mpvipc::*; use mpvipc_async::*;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), MpvError> { async fn main() -> Result<(), MpvError> {
let mpv = Mpv::connect("/tmp/mpv.sock").await?; let mpv = Mpv::connect("/tmp/mpv.sock").await?;
let paused: bool = mpv.get_property("pause").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] #[tokio::main]
async fn main() -> Result<(), MpvError> { async fn main() -> Result<(), MpvError> {
@ -12,8 +12,8 @@ async fn main() -> Result<(), MpvError> {
let playlist = mpv.get_playlist().await?; let playlist = mpv.get_playlist().await?;
println!("playlist: {:?}", playlist); println!("playlist: {:?}", playlist);
let playback_time: f64 = mpv.get_property("playback-time").await?; let playback_time: Option<f64> = mpv.get_property("playback-time").await?;
println!("playback-time: {}", playback_time); println!("playback-time: {:?}", playback_time);
Ok(()) Ok(())
} }

View File

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

View File

@ -5,12 +5,12 @@
fenix.inputs.nixpkgs.follows = "nixpkgs"; fenix.inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = { self, nixpkgs, fenix }@inputs: outputs = { self, nixpkgs, fenix }@inputs:
let let
systems = [ systems = [
"x86_64-linux" "x86_64-linux"
"aarch64-linux" "aarch64-linux"
"x86_64-darwin"
"aarch64-darwin" "aarch64-darwin"
]; ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: let forAllSystems = f: nixpkgs.lib.genAttrs systems (system: let
@ -30,8 +30,9 @@
]) ])
pkgs.mpv pkgs.mpv
pkgs.grcov 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. /// the upstream list of commands.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MpvCommand { pub enum MpvCommand {
/// Load the given file or URL and play it.
LoadFile { LoadFile {
file: String, file: String,
option: PlaylistAddOptions, option: PlaylistAddOptions,
}, },
/// Load the given playlist file or URL.
LoadList { LoadList {
file: String, file: String,
option: PlaylistAddOptions, option: PlaylistAddOptions,
}, },
/// Clear the playlist, except for the currently playing file.
PlaylistClear, PlaylistClear,
PlaylistMove {
from: usize, ///Move the playlist entry at `from`, so that it takes the place of the entry `to`.
to: usize, /// (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
Observe { /// the entry will have after moving.)
id: usize, PlaylistMove { from: usize, to: usize },
property: String,
}, /// 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, PlaylistNext,
/// Skip to the previous entry in the playlist.
PlaylistPrev, PlaylistPrev,
/// Remove an entry from the playlist by its position in the playlist.
PlaylistRemove(usize), PlaylistRemove(usize),
/// Shuffle the playlist
PlaylistShuffle, PlaylistShuffle,
/// Exit the player
Quit, 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>), ScriptMessage(Vec<String>),
ScriptMessageTo {
target: String, /// Same as [`MpvCommand::ScriptMessage`], but send the message to a specific target.
args: Vec<String>, ScriptMessageTo { target: String, args: Vec<String> },
},
Seek { /// Change the playback position.
seconds: f64, Seek { seconds: f64, option: SeekOptions },
option: SeekOptions,
}, /// Stop the current playback, and clear the playlist.
/// This esentially resets the entire player state without exiting mpv.
Stop, 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. /// 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. /// Generic data type representing all possible data types that mpv can return.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MpvDataType { pub enum MpvDataType {
Array(Vec<MpvDataType>), Array(Vec<MpvDataType>),
Bool(bool), Bool(bool),
@ -82,7 +109,7 @@ pub enum MpvDataType {
} }
/// A mpv playlist. /// A mpv playlist.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Playlist(pub Vec<PlaylistEntry>); pub struct Playlist(pub Vec<PlaylistEntry>);
/// A single entry in the mpv playlist. /// A single entry in the mpv playlist.
@ -90,7 +117,7 @@ pub struct Playlist(pub Vec<PlaylistEntry>);
pub struct PlaylistEntry { pub struct PlaylistEntry {
pub id: usize, pub id: usize,
pub filename: String, pub filename: String,
pub title: String, pub title: Option<String>,
pub current: bool, pub current: bool,
} }
@ -134,18 +161,22 @@ impl IntoRawCommandPart for SeekOptions {
pub trait GetPropertyTypeHandler: Sized { pub trait GetPropertyTypeHandler: Sized {
// TODO: fix this // TODO: fix this
#[allow(async_fn_in_trait)] #[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 impl<T> GetPropertyTypeHandler for T
where where
T: TypeHandler, 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 instance
.get_property_value(property) .get_property_value(property)
.await .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 instance
.command_sender .command_sender
.send(( .send((
MpvIpcCommand::SetProperty(property.to_owned(), value), MpvIpcCommand::SetProperty(property.to_owned(), value.to_owned()),
res_tx, res_tx,
)) ))
.await .await
@ -277,7 +308,7 @@ impl Mpv {
command: &str, command: &str,
args: &[&str], args: &[&str],
) -> Result<Option<Value>, MpvError> { ) -> Result<Option<Value>, MpvError> {
let command = Vec::from( let command_vec = Vec::from(
[command] [command]
.iter() .iter()
.chain(args.iter()) .chain(args.iter())
@ -287,7 +318,7 @@ impl Mpv {
); );
let (res_tx, res_rx) = oneshot::channel(); let (res_tx, res_rx) = oneshot::channel();
self.command_sender self.command_sender
.send((MpvIpcCommand::Command(command), res_tx)) .send((MpvIpcCommand::Command(command_vec.clone()), res_tx))
.await .await
.map_err(|err| MpvError::InternalConnectionError(err.to_string()))?; .map_err(|err| MpvError::InternalConnectionError(err.to_string()))?;
@ -317,7 +348,7 @@ impl Mpv {
/// ///
/// # Example /// # Example
/// ``` /// ```
/// use mpvipc::{Mpv, MpvError}; /// use mpvipc_async::{Mpv, MpvError};
/// ///
/// #[tokio::main] /// #[tokio::main]
/// async fn main() -> Result<(), MpvError> { /// async fn main() -> Result<(), MpvError> {
@ -449,7 +480,7 @@ impl Mpv {
/// ///
/// # Example /// # Example
/// ``` /// ```
/// use mpvipc::{Mpv, MpvError}; /// use mpvipc_async::{Mpv, MpvError};
/// ///
/// #[tokio::main] /// #[tokio::main]
/// async fn main() -> Result<(), MpvError> { /// async fn main() -> Result<(), MpvError> {
@ -462,7 +493,7 @@ impl Mpv {
pub async fn get_property<T: GetPropertyTypeHandler>( pub async fn get_property<T: GetPropertyTypeHandler>(
&self, &self,
property: &str, property: &str,
) -> Result<T, MpvError> { ) -> Result<Option<T>, MpvError> {
T::get_property_generic(self, property).await T::get_property_generic(self, property).await
} }
@ -478,7 +509,7 @@ impl Mpv {
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use mpvipc::{Mpv, MpvError}; /// use mpvipc_async::{Mpv, MpvError};
/// ///
/// #[tokio::main] /// #[tokio::main]
/// async fn main() -> Result<(), MpvError> { /// async fn main() -> Result<(), MpvError> {
@ -487,7 +518,7 @@ impl Mpv {
/// Ok(()) /// 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(); let (res_tx, res_rx) = oneshot::channel();
self.command_sender self.command_sender
.send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx)) .send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx))
@ -495,9 +526,7 @@ impl Mpv {
.map_err(|err| MpvError::InternalConnectionError(err.to_string()))?; .map_err(|err| MpvError::InternalConnectionError(err.to_string()))?;
match res_rx.await { match res_rx.await {
Ok(MpvIpcResponse(response)) => { Ok(MpvIpcResponse(response)) => response,
response.and_then(|value| value.ok_or(MpvError::MissingMpvData))
}
Err(err) => Err(MpvError::InternalConnectionError(err.to_string())), Err(err) => Err(MpvError::InternalConnectionError(err.to_string())),
} }
} }
@ -519,18 +548,17 @@ impl Mpv {
/// ///
/// # Example /// # Example
/// ``` /// ```
/// use mpvipc::{Mpv, MpvError}; /// use mpvipc_async::{Mpv, MpvError};
/// async fn main() -> Result<(), MpvError> { /// async fn main() -> Result<(), MpvError> {
/// let mpv = Mpv::connect("/tmp/mpvsocket").await?; /// let mpv = Mpv::connect("/tmp/mpvsocket").await?;
/// mpv.set_property("pause", true).await?; /// mpv.set_property("pause", true).await?;
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
pub async fn set_property<T: SetPropertyTypeHandler<T>>( pub async fn set_property<T>(&self, property: &str, value: T) -> Result<(), MpvError>
&self, where
property: &str, T: SetPropertyTypeHandler<T> + Clone + fmt::Debug,
value: T, {
) -> Result<(), MpvError> { T::set_property_generic(self, property, value.clone()).await
T::set_property_generic(self, property, value).await
} }
} }

View File

@ -3,13 +3,16 @@
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use thiserror::Error; use thiserror::Error;
use crate::MpvDataType; use crate::{MpvDataType, Property};
/// Any error that can occur when interacting with mpv. /// Any error that can occur when interacting with mpv.
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum MpvError { pub enum MpvError {
#[error("MpvError: {0}")] #[error("Mpv returned error in response to command: {message}\nCommand: {command:#?}")]
MpvError(String), MpvError {
command: Vec<Value>,
message: String,
},
#[error("Error communicating over mpv socket: {0}")] #[error("Error communicating over mpv socket: {0}")]
MpvSocketConnectionError(String), MpvSocketConnectionError(String),
@ -43,6 +46,63 @@ pub enum MpvError {
map: Map<String, Value>, map: Map<String, Value>,
}, },
#[error("Unexpected property: {0:?}")]
UnexpectedProperty(Property),
#[error("Unknown error: {0}")] #[error("Unknown error: {0}")]
Other(String), 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, VideoReconfig,
AudioReconfig, AudioReconfig,
PropertyChange { PropertyChange {
id: usize, id: Option<u64>,
name: String, name: String,
data: Option<MpvDataType>, data: Option<MpvDataType>,
}, },
@ -209,6 +209,12 @@ pub(crate) fn parse_event(raw_event: MpvIpcEvent) -> Result<Event, MpvError> {
"shutdown" => Ok(Event::Shutdown), "shutdown" => Ok(Event::Shutdown),
"log-message" => parse_log_message(event), "log-message" => parse_log_message(event),
"hook" => parse_hook(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" => // "get-property-reply" =>
// "set-property-reply" => // "set-property-reply" =>
// "command-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> { 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 property_name = get_key_as!(as_str, "name", event);
let data = event.get("data").map(json_to_value).transpose()?; let data = event.get("data").map(json_to_value).transpose()?;

View File

@ -1,8 +1,8 @@
//! High-level API extension for [`Mpv`]. //! High-level API extension for [`Mpv`].
use crate::{ use crate::{
IntoRawCommandPart, Mpv, MpvCommand, MpvDataType, MpvError, Playlist, PlaylistAddOptions, parse_property, IntoRawCommandPart, LoopProperty, Mpv, MpvCommand, MpvDataType, MpvError,
PlaylistEntry, SeekOptions, Playlist, PlaylistAddOptions, Property, SeekOptions,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@ -44,35 +44,7 @@ pub enum PlaylistAddTypeOptions {
// TODO: fix this // TODO: fix this
#[allow(async_fn_in_trait)] #[allow(async_fn_in_trait)]
pub trait MpvExt { pub trait MpvExt {
/// Stop the player completely (as opposed to pausing), // COMMANDS
/// 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. /// Seek to a specific position in the current video.
async fn seek(&self, seconds: f64, option: SeekOptions) -> Result<(), MpvError>; 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. /// Notify mpv to send events whenever a property changes.
/// See [`Mpv::get_event_stream`] and [`Property`](crate::Property) for more information. /// 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. /// Stop observing a property.
/// See [`Mpv::get_event_stream`] and [`Property`](crate::Property) for more information. /// 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. /// Skip to the next entry in the playlist.
async fn next(&self) -> Result<(), MpvError>; 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. /// it to exit itself. If mpv is stuck, it may not respond to this command.
async fn kill(&self) -> Result<(), MpvError>; 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. /// Get a list of all entries in the playlist.
async fn get_playlist(&self) -> Result<Playlist, MpvError>; async fn get_playlist(&self) -> Result<Playlist, MpvError>;
/// Get metadata about the current video. /// Get metadata about the current video.
async fn get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError>; 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 { impl MpvExt for Mpv {
async fn get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError> { // COMMANDS
self.get_property("metadata").await
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> { async fn playlist_shuffle(&self) -> Result<(), MpvError> {
self.get_property::<Vec<PlaylistEntry>>("playlist") self.run_command(MpvCommand::PlaylistShuffle).await
.await
.map(Playlist)
} }
async fn kill(&self) -> Result<(), MpvError> { async fn playlist_remove_id(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::Quit).await self.run_command(MpvCommand::PlaylistRemove(id)).await
} }
async fn next(&self) -> Result<(), MpvError> { async fn playlist_play_next(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistNext).await 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 observe_property(&self, id: usize, property: &str) -> Result<(), MpvError> { self.run_command(MpvCommand::PlaylistMove {
self.run_command(MpvCommand::Observe { from: id,
id, to: current_id + 1,
property: property.to_string(),
}) })
.await .await
} }
async fn unobserve_property(&self, id: usize) -> Result<(), MpvError> { async fn playlist_play_id(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::Unobserve(id)).await self.set_property("playlist-pos", id).await
} }
async fn set_playback(&self, option: Switch) -> Result<(), MpvError> { async fn playlist_move_id(&self, from: usize, to: usize) -> Result<(), MpvError> {
let enabled = match option { self.run_command(MpvCommand::PlaylistMove { from, to })
Switch::On => "yes",
Switch::Off => "no",
Switch::Toggle => {
self.get_property::<String>("pause")
.await .await
.map(|s| match s.as_str() {
"yes" => "no",
"no" => "yes",
_ => "no",
})?
}
};
self.set_property("pause", enabled).await
} }
async fn prev(&self) -> Result<(), MpvError> { async fn playlist_clear(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistPrev).await 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_add( async fn playlist_add(
@ -224,93 +247,60 @@ impl MpvExt for Mpv {
} }
} }
async fn playlist_clear(&self) -> Result<(), MpvError> { async fn restart(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistClear).await self.run_command(MpvCommand::Seek {
} seconds: 0f64,
option: SeekOptions::Absolute,
async fn playlist_move_id(&self, from: usize, to: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistMove { from, to })
.await
}
async fn playlist_play_id(&self, id: usize) -> Result<(), MpvError> {
self.set_property("playlist-pos", id).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 .await
} }
Err(msg) => Err(msg),
} async fn prev(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistPrev).await
} }
async fn playlist_remove_id(&self, id: usize) -> Result<(), MpvError> { async fn observe_property(&self, id: u64, property: &str) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistRemove(id)).await self.run_command(MpvCommand::Observe {
} id,
property: property.to_string(),
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 .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> { async fn unobserve_property(&self, id: u64) -> Result<(), MpvError> {
let enabled = match option { self.run_command(MpvCommand::Unobserve(id)).await
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> { async fn next(&self) -> Result<(), MpvError> {
let enabled = match option { self.run_command(MpvCommand::PlaylistNext).await
Switch::On => "yes", }
Switch::Off => "no",
Switch::Toggle => { async fn kill(&self) -> Result<(), MpvError> {
self.get_property::<String>("mute") self.run_command(MpvCommand::Quit).await
.await }
.map(|s| match s.as_str() {
"yes" => "no", async fn stop(&self) -> Result<(), MpvError> {
"no" => "yes", self.run_command(MpvCommand::Stop).await
_ => "no", }
})?
// 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
}
NumberChangeOptions::Decrease => {
self.set_property("volume", volume - input_volume).await
}
NumberChangeOptions::Absolute => self.set_property("volume", input_volume).await,
} }
};
self.set_property("mute", enabled).await
} }
async fn set_speed( async fn set_speed(
@ -318,44 +308,176 @@ impl MpvExt for Mpv {
input_speed: f64, input_speed: f64,
option: NumberChangeOptions, option: NumberChangeOptions,
) -> Result<(), MpvError> { ) -> Result<(), MpvError> {
match self.get_property::<f64>("speed").await { let speed = self.get_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
}
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, NumberChangeOptions::Absolute => self.set_property("speed", input_speed).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
}
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
}
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_volume( async fn set_loop_file(&self, option: Switch) -> Result<(), MpvError> {
&self, let enabled = match option {
input_volume: f64, Switch::On => "inf",
option: NumberChangeOptions, Switch::Off => "no",
) -> Result<(), MpvError> { Switch::Toggle => match self.file_is_looping().await? {
match self.get_property::<f64>("volume").await { LoopProperty::Inf => "no",
Ok(volume) => match option { LoopProperty::N(_) => "no",
NumberChangeOptions::Increase => { LoopProperty::No => "inf",
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,
}, },
Err(msg) => Err(msg), };
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> { async fn get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError> {
self.run_command(MpvCommand::Stop).await 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>), Command(Vec<String>),
GetProperty(String), GetProperty(String),
SetProperty(String, Value), SetProperty(String, Value),
ObserveProperty(usize, String), ObserveProperty(u64, String),
UnobserveProperty(usize), UnobserveProperty(u64),
Exit, Exit,
} }
@ -92,7 +92,7 @@ impl MpvIpc {
log::trace!("Received response: {:?}", response); log::trace!("Received response: {:?}", response);
parse_mpv_response_data(response?) parse_mpv_response_data(response?, command)
} }
pub(crate) async fn get_mpv_property( pub(crate) async fn get_mpv_property(
@ -114,17 +114,14 @@ impl MpvIpc {
pub(crate) async fn observe_property( pub(crate) async fn observe_property(
&mut self, &mut self,
id: usize, id: u64,
property: &str, property: &str,
) -> Result<Option<Value>, MpvError> { ) -> Result<Option<Value>, MpvError> {
self.send_command(&[json!("observe_property"), json!(id), json!(property)]) self.send_command(&[json!("observe_property"), json!(id), json!(property)])
.await .await
} }
pub(crate) async fn unobserve_property( pub(crate) async fn unobserve_property(&mut self, id: u64) -> Result<Option<Value>, MpvError> {
&mut self,
id: usize,
) -> Result<Option<Value>, MpvError> {
self.send_command(&[json!("unobserve_property"), json!(id)]) self.send_command(&[json!("unobserve_property"), json!(id)])
.await .await
} }
@ -197,7 +194,7 @@ impl MpvIpc {
/// This function does the most basic JSON parsing and error handling /// This function does the most basic JSON parsing and error handling
/// for status codes and errors that all responses from mpv are /// for status codes and errors that all responses from mpv are
/// expected to contain. /// 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); log::trace!("Parsing mpv response data: {:?}", value);
let result = value let result = value
.as_object() .as_object()
@ -224,7 +221,11 @@ fn parse_mpv_response_data(value: Value) -> Result<Option<Value>, MpvError> {
}) })
.and_then(|(error, data)| match error { .and_then(|(error, data)| match error {
"success" => Ok(data), "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 { match &result {

View File

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

View File

@ -109,7 +109,7 @@ impl TypeHandler for Vec<PlaylistEntry> {
expected_type: "Array<Value>".to_string(), expected_type: "Array<Value>".to_string(),
received: value.clone(), received: value.clone(),
}) })
.map(|array| json_array_to_playlist(array)) .and_then(|array| json_array_to_playlist(array))
} }
fn as_string(&self) -> String { 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() array.iter().map(json_to_value).collect()
} }
pub(crate) fn json_array_to_playlist(array: &[Value]) -> Vec<PlaylistEntry> { fn json_map_to_playlist_entry(
let mut output: Vec<PlaylistEntry> = Vec::new(); map: &serde_json::map::Map<String, Value>,
for (id, entry) in array.iter().enumerate() { ) -> Result<PlaylistEntry, MpvError> {
let mut filename: String = String::new(); let filename = match map.get("filename") {
let mut title: String = String::new(); Some(Value::String(s)) => s.to_string(),
let mut current: bool = false; Some(data) => {
if let Value::String(ref f) = entry["filename"] { return Err(MpvError::ValueContainsUnexpectedType {
filename = f.to_string(); expected_type: "String".to_owned(),
received: data.clone(),
})
} }
if let Value::String(ref t) = entry["title"] { None => return Err(MpvError::MissingMpvData),
title = t.to_string(); };
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"] { None => None,
current = *b; };
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 { None => false,
id, };
Ok(PlaylistEntry {
id: 0,
filename, filename,
title, title,
current, current,
}); })
} }
output
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)] #[cfg(test)]
@ -277,7 +313,7 @@ mod test {
} }
#[test] #[test]
fn test_json_array_to_playlist() { fn test_json_array_to_playlist() -> Result<(), MpvError> {
let json = json!([ let json = json!([
{ {
"filename": "file1", "filename": "file1",
@ -288,6 +324,10 @@ mod test {
"filename": "file2", "filename": "file2",
"title": "title2", "title": "title2",
"current": false "current": false
},
{
"filename": "file3",
"current": false
} }
]); ]);
@ -295,17 +335,25 @@ mod test {
PlaylistEntry { PlaylistEntry {
id: 0, id: 0,
filename: "file1".to_string(), filename: "file1".to_string(),
title: "title1".to_string(), title: Some("title1".to_string()),
current: true, current: true,
}, },
PlaylistEntry { PlaylistEntry {
id: 1, id: 1,
filename: "file2".to_string(), 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, 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 //! This module is used to parse the json data from the `data` field of
//! [`Event::PropertyChange`] variant. Mpv has about 1000 different properties //! known properties. Mpv has about 1000 different properties
//! as of `v0.38.0`, so this module will only implement the most common ones. //! 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 std::collections::HashMap;
use serde::{Deserialize, Serialize}; 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. /// Unimplemented properties will be returned with it's data
/// If something is missing, please open an issue. /// as a `Property::Unknown` variant.
///
/// Otherwise, the property will be returned as a `Property::Unknown` variant.
/// ///
/// See <https://mpv.io/manual/master/#properties> for /// See <https://mpv.io/manual/master/#properties> for
/// the upstream list of properties. /// the upstream list of properties.
@ -31,9 +34,12 @@ pub enum Property {
PlaylistPos(Option<usize>), PlaylistPos(Option<usize>),
LoopFile(LoopProperty), LoopFile(LoopProperty),
LoopPlaylist(LoopProperty), LoopPlaylist(LoopProperty),
TimePos(Option<f64>),
TimeRemaining(Option<f64>),
Speed(f64), Speed(f64),
Volume(f64), Volume(f64),
Mute(bool), Mute(bool),
EofReached(bool),
Unknown { Unknown {
name: String, name: String,
data: Option<MpvDataType>, data: Option<MpvDataType>,
@ -48,17 +54,12 @@ pub enum LoopProperty {
No, No,
} }
/// Parse a highlevel [`Property`] object from json, used for [`Event::PropertyChange`]. /// Parse a highlevel [`Property`] object from mpv data.
pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError> { ///
let (id, name, data) = match event { /// This is intended to be used with the `data` field of
Event::PropertyChange { id, name, data } => (id, name, data), /// `Event::PropertyChange` and the response from `Mpv::get_property_value()`.
// TODO: return proper error pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property, MpvError> {
_ => { match name {
panic!("Event is not a PropertyChange event")
}
};
match name.as_str() {
"path" => { "path" => {
let path = match data { let path = match data {
Some(MpvDataType::String(s)) => Some(s), 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); return Err(MpvError::MissingMpvData);
} }
}; };
Ok((id, Property::Path(path))) Ok(Property::Path(path))
} }
"pause" => { "pause" => {
let pause = match data { let pause = match data {
@ -88,7 +89,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
return Err(MpvError::MissingMpvData); return Err(MpvError::MissingMpvData);
} }
}; };
Ok((id, Property::Pause(pause))) Ok(Property::Pause(pause))
} }
"playback-time" => { "playback-time" => {
let playback_time = match data { 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" => { "duration" => {
let duration = match data { 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" => { "metadata" => {
let metadata = match data { 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" => { "playlist-pos" => {
let playlist_pos = match data { let playlist_pos = match data {
Some(MpvDataType::Usize(u)) => Some(u), 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" => { "loop-file" => {
let loop_file = match data.to_owned() { 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, None => MpvError::MissingMpvData,
})?; })?;
Ok((id, Property::LoopFile(loop_file))) Ok(Property::LoopFile(loop_file))
} }
"loop-playlist" => { "loop-playlist" => {
let loop_playlist = match data.to_owned() { 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, 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" => { "speed" => {
let speed = match data { let speed = match data {
@ -210,7 +243,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
return Err(MpvError::MissingMpvData); return Err(MpvError::MissingMpvData);
} }
}; };
Ok((id, Property::Speed(speed))) Ok(Property::Speed(speed))
} }
"volume" => { "volume" => {
let volume = match data { let volume = match data {
@ -225,7 +258,7 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
return Err(MpvError::MissingMpvData); return Err(MpvError::MissingMpvData);
} }
}; };
Ok((id, Property::Volume(volume))) Ok(Property::Volume(volume))
} }
"mute" => { "mute" => {
let mute = match data { let mute = match data {
@ -240,9 +273,81 @@ pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError>
return Err(MpvError::MissingMpvData); 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 // 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 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 thiserror::Error;
use tokio::time::sleep; use tokio::time::sleep;
use tokio::time::{timeout, Duration}; use tokio::time::{timeout, Duration};
@ -8,17 +8,22 @@ use test_log::test;
use super::*; use super::*;
const MPV_CHANNEL_ID: usize = 1337; const MPV_CHANNEL_ID: u64 = 1337;
#[derive(Error, Debug)] #[derive(Error, Debug)]
enum PropertyCheckingThreadError { enum PropertyCheckingThreadError {
#[error("Unexpected property: {0:?}")] #[error("Unexpected property: {0:?}")]
UnexpectedPropertyError(mpvipc::Property), UnexpectedPropertyError(Property),
#[error(transparent)] #[error(transparent)]
MpvError(#[from] MpvError), 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>( fn create_interruptable_event_property_checking_thread<T>(
mut events: impl Stream<Item = Result<Event, MpvError>> + Unpin + Send + 'static, mut events: impl Stream<Item = Result<Event, MpvError>> + Unpin + Send + 'static,
on_property: T, on_property: T,
@ -27,7 +32,7 @@ fn create_interruptable_event_property_checking_thread<T>(
tokio_util::sync::CancellationToken, tokio_util::sync::CancellationToken,
) )
where 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 = tokio_util::sync::CancellationToken::new();
let cancellation_token_clone = cancellation_token.clone(); let cancellation_token_clone = cancellation_token.clone();
@ -38,8 +43,8 @@ where
match event { match event {
Some(Ok(event)) => { Some(Ok(event)) => {
match event { match event {
Event::PropertyChange { id: MPV_CHANNEL_ID, .. } => { Event::PropertyChange { id: Some(MPV_CHANNEL_ID), name, data } => {
let property = parse_event_property(event).unwrap().1; let property = parse_property(&name, data).unwrap();
if !on_property(property.clone()) { if !on_property(property.clone()) {
return Err(PropertyCheckingThreadError::UnexpectedPropertyError(property)) return Err(PropertyCheckingThreadError::UnexpectedPropertyError(property))
} }
@ -61,6 +66,9 @@ where
(handle, cancellation_token) (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( async fn graceful_shutdown(
cancellation_token: tokio_util::sync::CancellationToken, cancellation_token: tokio_util::sync::CancellationToken,
handle: tokio::task::JoinHandle<Result<(), PropertyCheckingThreadError>>, handle: tokio::task::JoinHandle<Result<(), PropertyCheckingThreadError>>,
@ -103,6 +111,7 @@ async fn graceful_shutdown(
Ok(()) Ok(())
} }
/// Test correct parsing of different values of the "pause" property
#[test(tokio::test)] #[test(tokio::test)]
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
async fn test_highlevel_event_pause() -> Result<(), MpvError> { 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 events = mpv.get_event_stream().await;
let (handle, cancellation_token) = let (handle, cancellation_token) =
create_interruptable_event_property_checking_thread(events, |property| match property { create_interruptable_event_property_checking_thread(events, |property| match property {
mpvipc::Property::Pause(_) => { Property::Pause(_) => {
log::debug!("{:?}", property); log::debug!("{:?}", property);
true true
} }
@ -131,6 +140,7 @@ async fn test_highlevel_event_pause() -> Result<(), MpvError> {
Ok(()) Ok(())
} }
/// Test correct parsing of different values of the "volume" property
#[test(tokio::test)] #[test(tokio::test)]
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
async fn test_highlevel_event_volume() -> Result<(), MpvError> { 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 events = mpv.get_event_stream().await;
let (handle, cancellation_token) = let (handle, cancellation_token) =
create_interruptable_event_property_checking_thread(events, |property| match property { create_interruptable_event_property_checking_thread(events, |property| match property {
mpvipc::Property::Volume(_) => { Property::Volume(_) => {
log::trace!("{:?}", property); log::trace!("{:?}", property);
true true
} }
@ -160,6 +170,7 @@ async fn test_highlevel_event_volume() -> Result<(), MpvError> {
Ok(()) Ok(())
} }
/// Test correct parsing of different values of the "mute" property
#[test(tokio::test)] #[test(tokio::test)]
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
async fn test_highlevel_event_mute() -> Result<(), MpvError> { 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 events = mpv.get_event_stream().await;
let (handle, cancellation_token) = let (handle, cancellation_token) =
create_interruptable_event_property_checking_thread(events, |property| match property { create_interruptable_event_property_checking_thread(events, |property| match property {
mpvipc::Property::Mute(_) => { Property::Mute(_) => {
log::trace!("{:?}", property); log::trace!("{:?}", property);
true true
} }
@ -187,6 +198,7 @@ async fn test_highlevel_event_mute() -> Result<(), MpvError> {
Ok(()) Ok(())
} }
/// Test correct parsing of different values of the "duration" property
#[test(tokio::test)] #[test(tokio::test)]
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
async fn test_highlevel_event_duration() -> Result<(), MpvError> { 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 events = mpv.get_event_stream().await;
let (handle, cancellation_token) = let (handle, cancellation_token) =
create_interruptable_event_property_checking_thread(events, |property| match property { create_interruptable_event_property_checking_thread(events, |property| match property {
mpvipc::Property::Duration(_) => { Property::Duration(_) => {
log::trace!("{:?}", property); log::trace!("{:?}", property);
true true
} }

View File

@ -1,26 +1,62 @@
use mpvipc::MpvExt; use mpvipc_async::{MpvError, MpvExt};
use super::*; use super::*;
#[tokio::test] #[tokio::test]
#[cfg(target_family = "unix")] #[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 (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")); assert!(version.starts_with("mpv"));
mpv.kill().await.unwrap(); mpv.kill().await.unwrap();
proc.kill().await.unwrap(); proc.kill().await.unwrap();
Ok(())
} }
#[tokio::test] #[tokio::test]
#[cfg(target_family = "unix")] #[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(); let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
mpv.set_property("pause", true).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); assert!(paused);
mpv.kill().await.unwrap(); mpv.kill().await.unwrap();
proc.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 std::{path::Path, time::Duration};
use mpvipc::{Mpv, MpvError}; use mpvipc_async::{Mpv, MpvError};
use tokio::{ use tokio::{
process::{Child, Command}, process::{Child, Command},
time::{sleep, timeout}, time::{sleep, timeout},
@ -25,7 +25,7 @@ pub async fn spawn_headless_mpv() -> Result<(Child, Mpv), MpvError> {
.spawn() .spawn()
.expect("Failed to start mpv"); .expect("Failed to start mpv");
timeout(Duration::from_millis(500), async { timeout(Duration::from_millis(1000), async {
while !&socket_path.exists() { while !&socket_path.exists() {
sleep(Duration::from_millis(10)).await; sleep(Duration::from_millis(10)).await;
} }

View File

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

View File

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

View File

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