Compare commits

..

1 Commits
main ... master

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

View File

@ -2,31 +2,42 @@ name: "Build and test"
on: on:
push: push:
branches: branches:
- main - master
pull_request: pull_request:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest-personal
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install rust toolchain - name: Install latest nightly toolchain
uses: dtolnay/rust-toolchain@stable uses: actions-rs/toolchain@v1
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 runs-on: ubuntu-latest-personal
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install rust toolchain - name: Install latest nightly toolchain
uses: dtolnay/rust-toolchain@stable uses: actions-rs/toolchain@v1
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
@ -34,27 +45,30 @@ jobs:
run: cargo clippy --all-features -- --deny warnings run: cargo clippy --all-features -- --deny warnings
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest-personal
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- 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 rust toolchain - name: Install latest nightly toolchain
uses: dtolnay/rust-toolchain@nightly uses: actions-rs/toolchain@v1
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 --test-threads=1 cargo nextest run --all-features --release --no-fail-fast
env: env:
RUST_LOG: "trace" RUST_LOG: "trace"
RUSTFLAGS: "-Cinstrument-coverage" RUSTFLAGS: "-Cinstrument-coverage"
@ -82,32 +96,38 @@ jobs:
target/coverage/ target/coverage/
- name: Upload test report - name: Upload test report
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1 uses: https://git.pvv.ntnu.no/oysteikt/rsync-action@main
with: with:
source: target/coverage/html/ source: target/coverage/html/
target: ${{ gitea.ref_name }}/coverage/ target: mpvipc/${{ gitea.ref_name }}/coverage/
username: gitea-web username: oysteikt
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }} ssh-key: ${{ secrets.OYSTEIKT_GITEA_WEBDOCS_SSH_KEY }}
host: bekkalokk.pvv.ntnu.no host: microbel.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx" known-hosts: "microbel.pvv.ntnu.no ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEq0yasKP0mH6PI6ypmuzPzMnbHELo9k+YB5yW534aKudKZS65YsHJKQ9vapOtmegrn5MQbCCgrshf+/XwZcjbM="
docs: docs:
runs-on: ubuntu-latest runs-on: ubuntu-latest-personal
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install rust toolchain - name: Install latest nightly toolchain
uses: dtolnay/rust-toolchain@stable uses: actions-rs/toolchain@v1
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/Projects/rsync-action@v1 uses: https://git.pvv.ntnu.no/oysteikt/rsync-action@main
with: with:
source: target/doc/ source: target/doc/
target: ${{ gitea.ref_name }}/docs/ target: mpvipc/${{ gitea.ref_name }}/docs/
username: gitea-web username: oysteikt
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }} ssh-key: ${{ secrets.OYSTEIKT_GITEA_WEBDOCS_SSH_KEY }}
host: bekkalokk.pvv.ntnu.no host: microbel.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx" known-hosts: "microbel.pvv.ntnu.no ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEq0yasKP0mH6PI6ypmuzPzMnbHELo9k+YB5yW534aKudKZS65YsHJKQ9vapOtmegrn5MQbCCgrshf+/XwZcjbM="

View File

@ -1,14 +1,15 @@
[package] [package]
name = "mpvipc-async" name = "mpvipc"
version = "0.1.0" version = "1.3.0"
authors = [ authors = [
"Jonas Frei <freijon@pm.me>", "Jonas Frei <freijon@pm.me>",
"Øystein Tveit <oysteikt@pvv.ntnu.no>" "h7x4 <h7x4@nani.wtf>"
] ]
description = "A small library which provides bindings to control existing mpv instances through sockets." description = "A small library which provides bindings to control existing mpv instances through sockets."
license = "GPL-3.0" license = "GPL-3.0"
repository = "https://git.pvv.ntnu.no/Grzegorz/mpvipc-async" homepage = "https://git.pvv.ntnu.no/oysteikt/mpvipc"
documentation = "https://pages.pvv.ntnu.no/Grzegorz/mpvipc-async/main/docs/mpvipc_async/" repository = "https://git.pvv.ntnu.no/oysteikt/mpvipc"
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,18 +1,16 @@
[![Coverage](https://pages.pvv.ntnu.no/Grzegorz/mpvipc-async/main/coverage/badges/for_the_badge.svg)](https://pages.pvv.ntnu.no/Grzegorz/mpvipc-async/main/coverage/src/) [![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://pages.pvv.ntnu.no/Grzegorz/mpvipc-async/main/docs/mpvipc_async/) [![Docs](https://img.shields.io/badge/docs-blue?style=for-the-badge&logo=rust)](https://pvv.ntnu.no/~oysteikt/gitea/mpvipc/master/docs/mpvipc/)
# mpvipc-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` (runtime dependency) - `mpv`
- `cargo-nextest` (optional test depencency) - `cargo` (make dependency)
- `grcov` (optional test depencency) - `cargo-nextest` (test depencency)
- `grcov` (test depencency)
## Example ## Example
@ -25,12 +23,12 @@ $ mpv --input-ipc-server=/tmp/mpv.sock --idle
Here is a small code example which connects to the socket `/tmp/mpv.sock` and toggles playback. Here is a small code example which connects to the socket `/tmp/mpv.sock` and toggles playback.
```rust ```rust
use mpvipc_async::*; use mpvipc::*;
#[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).await.expect("Error pausing"); mpv.set_property("pause", !paused).expect("Error pausing");
} }
``` ```

View File

@ -1,4 +1,4 @@
use mpvipc_async::{Mpv, MpvError, MpvExt}; use mpvipc::{Mpv, MpvError, MpvExt};
#[tokio::main] #[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: Option<f64> = mpv.get_property("playback-time").await?; let playback_time: 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_async::{parse_property, Event, Mpv, MpvDataType, MpvError, MpvExt, Property}; use mpvipc::{parse_event_property, Event, Mpv, MpvDataType, MpvError, MpvExt, Property};
fn seconds_to_hms(total: f64) -> String { fn seconds_to_hms(total: f64) -> String {
let total = total as u64; let total = total as u64;
@ -25,36 +25,34 @@ 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_async::Event::PropertyChange { name, data, .. } => { mpvipc::Event::PropertyChange { .. } => match parse_event_property(event)? {
match parse_property(&name, data)? { (1, Property::Path(Some(value))) => println!("\nPlaying: {}", value),
Property::Path(Some(value)) => println!("\nPlaying: {}", value), (2, Property::Pause(value)) => {
Property::Pause(value) => { println!("Pause: {}", value);
println!("Pause: {}", value);
}
Property::PlaybackTime(Some(value)) => {
println!("Playback time: {}", seconds_to_hms(value));
}
Property::Duration(Some(value)) => {
println!("Duration: {}", seconds_to_hms(value));
}
Property::Metadata(Some(value)) => {
println!("File tags:");
if let Some(MpvDataType::String(value)) = value.get("ARTIST") {
println!(" Artist: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("ALBUM") {
println!(" Album: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("TITLE") {
println!(" Title: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("TRACK") {
println!(" Track: {}", value);
}
}
_ => (),
} }
} (3, Property::PlaybackTime(Some(value))) => {
println!("Playback time: {}", seconds_to_hms(value));
}
(4, Property::Duration(Some(value))) => {
println!("Duration: {}", seconds_to_hms(value));
}
(5, Property::Metadata(Some(value))) => {
println!("File tags:");
if let Some(MpvDataType::String(value)) = value.get("ARTIST") {
println!(" Artist: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("ALBUM") {
println!(" Album: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("TITLE") {
println!(" Title: {}", value);
}
if let Some(MpvDataType::String(value)) = value.get("TRACK") {
println!(" Track: {}", value);
}
}
_ => (),
},
Event::Shutdown => return Ok(()), Event::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,9 +30,8 @@
]) ])
pkgs.mpv pkgs.mpv
pkgs.grcov pkgs.grcov
pkgs.cargo-nextest
]; ];
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library"; RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/";
}); });
}; };
} }

View File

@ -27,65 +27,39 @@ use crate::{
/// the upstream list of commands. /// 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 {
///Move the playlist entry at `from`, so that it takes the place of the entry `to`. from: usize,
/// (Paradoxically, the moved playlist entry will not have the index value `to` after moving to: usize,
/// if `from` was lower than `to`, because `to` refers to the target entry, not the index },
/// the entry will have after moving.) Observe {
PlaylistMove { from: usize, to: usize }, id: 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 {
/// Same as [`MpvCommand::ScriptMessage`], but send the message to a specific target. target: String,
ScriptMessageTo { target: String, args: Vec<String> }, args: Vec<String>,
},
/// Change the playback position. Seek {
Seek { seconds: f64, option: SeekOptions }, seconds: f64,
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.
@ -95,7 +69,6 @@ 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),
@ -109,7 +82,7 @@ pub enum MpvDataType {
} }
/// A mpv playlist. /// A mpv playlist.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, 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.
@ -117,7 +90,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: Option<String>, pub title: String,
pub current: bool, pub current: bool,
} }
@ -161,22 +134,18 @@ 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) async fn get_property_generic(instance: &Mpv, property: &str) -> Result<Self, MpvError>;
-> 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<Option<T>, MpvError> { async fn get_property_generic(instance: &Mpv, property: &str) -> Result<T, MpvError> {
instance instance
.get_property_value(property) .get_property_value(property)
.await .await
.and_then(|value| match value { .and_then(T::get_value)
Some(v) => T::get_value(v).map(|v| Some(v)),
None => Ok(None),
})
} }
} }
@ -203,7 +172,7 @@ where
instance instance
.command_sender .command_sender
.send(( .send((
MpvIpcCommand::SetProperty(property.to_owned(), value.to_owned()), MpvIpcCommand::SetProperty(property.to_owned(), value),
res_tx, res_tx,
)) ))
.await .await
@ -308,7 +277,7 @@ impl Mpv {
command: &str, command: &str,
args: &[&str], args: &[&str],
) -> Result<Option<Value>, MpvError> { ) -> Result<Option<Value>, MpvError> {
let command_vec = Vec::from( let command = Vec::from(
[command] [command]
.iter() .iter()
.chain(args.iter()) .chain(args.iter())
@ -318,7 +287,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_vec.clone()), res_tx)) .send((MpvIpcCommand::Command(command), res_tx))
.await .await
.map_err(|err| MpvError::InternalConnectionError(err.to_string()))?; .map_err(|err| MpvError::InternalConnectionError(err.to_string()))?;
@ -348,7 +317,7 @@ impl Mpv {
/// ///
/// # Example /// # Example
/// ``` /// ```
/// use mpvipc_async::{Mpv, MpvError}; /// use mpvipc::{Mpv, MpvError};
/// ///
/// #[tokio::main] /// #[tokio::main]
/// async fn main() -> Result<(), MpvError> { /// async fn main() -> Result<(), MpvError> {
@ -480,7 +449,7 @@ impl Mpv {
/// ///
/// # Example /// # Example
/// ``` /// ```
/// use mpvipc_async::{Mpv, MpvError}; /// use mpvipc::{Mpv, MpvError};
/// ///
/// #[tokio::main] /// #[tokio::main]
/// async fn main() -> Result<(), MpvError> { /// async fn main() -> Result<(), MpvError> {
@ -493,7 +462,7 @@ impl Mpv {
pub async fn get_property<T: GetPropertyTypeHandler>( pub async fn get_property<T: GetPropertyTypeHandler>(
&self, &self,
property: &str, property: &str,
) -> Result<Option<T>, MpvError> { ) -> Result<T, MpvError> {
T::get_property_generic(self, property).await T::get_property_generic(self, property).await
} }
@ -509,7 +478,7 @@ impl Mpv {
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use mpvipc_async::{Mpv, MpvError}; /// use mpvipc::{Mpv, MpvError};
/// ///
/// #[tokio::main] /// #[tokio::main]
/// async fn main() -> Result<(), MpvError> { /// async fn main() -> Result<(), MpvError> {
@ -518,7 +487,7 @@ impl Mpv {
/// Ok(()) /// Ok(())
/// } /// }
/// ``` /// ```
pub async fn get_property_value(&self, property: &str) -> Result<Option<Value>, MpvError> { pub async fn get_property_value(&self, property: &str) -> Result<Value, MpvError> {
let (res_tx, res_rx) = oneshot::channel(); 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))
@ -526,7 +495,9 @@ 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)) => response, Ok(MpvIpcResponse(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())),
} }
} }
@ -548,17 +519,18 @@ impl Mpv {
/// ///
/// # Example /// # Example
/// ``` /// ```
/// use mpvipc_async::{Mpv, MpvError}; /// use mpvipc::{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>(&self, property: &str, value: T) -> Result<(), MpvError> pub async fn set_property<T: SetPropertyTypeHandler<T>>(
where &self,
T: SetPropertyTypeHandler<T> + Clone + fmt::Debug, property: &str,
{ value: T,
T::set_property_generic(self, property, value.clone()).await ) -> Result<(), MpvError> {
T::set_property_generic(self, property, value).await
} }
} }

View File

@ -3,16 +3,13 @@
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use thiserror::Error; use thiserror::Error;
use crate::{MpvDataType, Property}; use crate::MpvDataType;
/// 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("Mpv returned error in response to command: {message}\nCommand: {command:#?}")] #[error("MpvError: {0}")]
MpvError { MpvError(String),
command: Vec<Value>,
message: String,
},
#[error("Error communicating over mpv socket: {0}")] #[error("Error communicating over mpv socket: {0}")]
MpvSocketConnectionError(String), MpvSocketConnectionError(String),
@ -46,63 +43,6 @@ 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: Option<u64>, id: usize,
name: String, name: String,
data: Option<MpvDataType>, data: Option<MpvDataType>,
}, },
@ -209,12 +209,6 @@ 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" =>
@ -296,7 +290,7 @@ fn parse_client_message(event: &Map<String, Value>) -> Result<Event, MpvError> {
} }
fn parse_property_change(event: &Map<String, Value>) -> Result<Event, MpvError> { fn parse_property_change(event: &Map<String, Value>) -> Result<Event, MpvError> {
let id = get_optional_key_as!(as_u64, "id", event); let id = get_key_as!(as_u64, "id", event) as usize;
let property_name = get_key_as!(as_str, "name", event); let 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,24 +1,21 @@
//! JSON parsing logic for properties returned by //! JSON parsing logic for properties returned in [`Event::PropertyChange`]
//! [`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 //! This module is used to parse the json data from the `data` field of the
//! known properties. Mpv has about 1000 different properties //! [`Event::PropertyChange`] variant. 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::{MpvDataType, MpvError, PlaylistEntry}; use crate::{Event, MpvDataType, MpvError, PlaylistEntry};
/// An incomplete list of properties that mpv can return. /// All possible properties that can be observed through the event system.
/// ///
/// Unimplemented properties will be returned with it's data /// Not all properties are guaranteed to be implemented.
/// as a `Property::Unknown` variant. /// If something is missing, please open an issue.
///
/// Otherwise, the property will be returned as a `Property::Unknown` variant.
/// ///
/// See <https://mpv.io/manual/master/#properties> for /// See <https://mpv.io/manual/master/#properties> for
/// the upstream list of properties. /// the upstream list of properties.
@ -34,12 +31,9 @@ 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>,
@ -54,12 +48,17 @@ pub enum LoopProperty {
No, No,
} }
/// Parse a highlevel [`Property`] object from mpv data. /// Parse a highlevel [`Property`] object from json, used for [`Event::PropertyChange`].
/// pub fn parse_event_property(event: Event) -> Result<(usize, Property), MpvError> {
/// This is intended to be used with the `data` field of let (id, name, data) = match event {
/// `Event::PropertyChange` and the response from `Mpv::get_property_value()`. Event::PropertyChange { id, name, data } => (id, name, data),
pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property, MpvError> { // TODO: return proper error
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),
@ -74,7 +73,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
return Err(MpvError::MissingMpvData); return Err(MpvError::MissingMpvData);
} }
}; };
Ok(Property::Path(path)) Ok((id, Property::Path(path)))
} }
"pause" => { "pause" => {
let pause = match data { let pause = match data {
@ -89,7 +88,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
return Err(MpvError::MissingMpvData); return Err(MpvError::MissingMpvData);
} }
}; };
Ok(Property::Pause(pause)) Ok((id, Property::Pause(pause)))
} }
"playback-time" => { "playback-time" => {
let playback_time = match data { let playback_time = match data {
@ -102,7 +101,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
}) })
} }
}; };
Ok(Property::PlaybackTime(playback_time)) Ok((id, Property::PlaybackTime(playback_time)))
} }
"duration" => { "duration" => {
let duration = match data { let duration = match data {
@ -115,7 +114,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
}) })
} }
}; };
Ok(Property::Duration(duration)) Ok((id, Property::Duration(duration)))
} }
"metadata" => { "metadata" => {
let metadata = match data { let metadata = match data {
@ -128,21 +127,16 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
}) })
} }
}; };
Ok(Property::Metadata(metadata)) Ok((id, 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),
@ -156,7 +150,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
}) })
} }
}; };
Ok(Property::PlaylistPos(playlist_pos)) Ok((id, Property::PlaylistPos(playlist_pos)))
} }
"loop-file" => { "loop-file" => {
let loop_file = match data.to_owned() { let loop_file = match data.to_owned() {
@ -178,7 +172,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
}, },
None => MpvError::MissingMpvData, None => MpvError::MissingMpvData,
})?; })?;
Ok(Property::LoopFile(loop_file)) Ok((id, Property::LoopFile(loop_file)))
} }
"loop-playlist" => { "loop-playlist" => {
let loop_playlist = match data.to_owned() { let loop_playlist = match data.to_owned() {
@ -201,34 +195,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
None => MpvError::MissingMpvData, None => MpvError::MissingMpvData,
})?; })?;
Ok(Property::LoopPlaylist(loop_playlist)) Ok((id, 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 {
@ -243,7 +210,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
return Err(MpvError::MissingMpvData); return Err(MpvError::MissingMpvData);
} }
}; };
Ok(Property::Speed(speed)) Ok((id, Property::Speed(speed)))
} }
"volume" => { "volume" => {
let volume = match data { let volume = match data {
@ -258,7 +225,7 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
return Err(MpvError::MissingMpvData); return Err(MpvError::MissingMpvData);
} }
}; };
Ok(Property::Volume(volume)) Ok((id, Property::Volume(volume)))
} }
"mute" => { "mute" => {
let mute = match data { let mute = match data {
@ -273,81 +240,9 @@ pub fn parse_property(name: &str, data: Option<MpvDataType>) -> Result<Property,
return Err(MpvError::MissingMpvData); return Err(MpvError::MissingMpvData);
} }
}; };
Ok(Property::Mute(mute)) Ok((id, 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(Property::Unknown { _ => Ok((id, Property::Unknown { name, data })),
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()
}

View File

@ -1,8 +1,8 @@
//! High-level API extension for [`Mpv`]. //! High-level API extension for [`Mpv`].
use crate::{ use crate::{
parse_property, IntoRawCommandPart, LoopProperty, Mpv, MpvCommand, MpvDataType, MpvError, IntoRawCommandPart, Mpv, MpvCommand, MpvDataType, MpvError, Playlist, PlaylistAddOptions,
Playlist, PlaylistAddOptions, Property, SeekOptions, PlaylistEntry, SeekOptions,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@ -44,7 +44,35 @@ 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 {
// COMMANDS /// Stop the player completely (as opposed to pausing),
/// removing the pointer to the current video.
async fn stop(&self) -> Result<(), MpvError>;
/// Set the volume of the player.
async fn set_volume(
&self,
input_volume: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError>;
/// Set the playback speed of the player.
async fn set_speed(
&self,
input_speed: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError>;
/// Toggle/set the pause state of the player.
async fn set_playback(&self, option: Switch) -> Result<(), MpvError>;
/// Toggle/set the mute state of the player.
async fn set_mute(&self, option: Switch) -> Result<(), MpvError>;
/// Toggle/set whether the player should loop the current playlist.
async fn set_loop_playlist(&self, option: Switch) -> Result<(), MpvError>;
/// Toggle/set whether the player should loop the current video.
async fn set_loop_file(&self, option: Switch) -> Result<(), MpvError>;
/// Seek to a specific position in the current video. /// 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>;
@ -88,11 +116,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: u64, property: &str) -> Result<(), MpvError>; async fn observe_property(&self, id: usize, 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: u64) -> Result<(), MpvError>; async fn unobserve_property(&self, id: usize) -> 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>;
@ -104,122 +132,71 @@ 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 {
// COMMANDS async fn get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError> {
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 playlist_shuffle(&self) -> Result<(), MpvError> { async fn get_playlist(&self) -> Result<Playlist, MpvError> {
self.run_command(MpvCommand::PlaylistShuffle).await self.get_property::<Vec<PlaylistEntry>>("playlist")
.await
.map(Playlist)
} }
async fn playlist_remove_id(&self, id: usize) -> Result<(), MpvError> { async fn kill(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistRemove(id)).await self.run_command(MpvCommand::Quit).await
} }
async fn playlist_play_next(&self, id: usize) -> Result<(), MpvError> { async fn next(&self) -> Result<(), MpvError> {
let data = self.get_property("playlist-pos").await?; self.run_command(MpvCommand::PlaylistNext).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 { async fn observe_property(&self, id: usize, property: &str) -> Result<(), MpvError> {
from: id, self.run_command(MpvCommand::Observe {
to: current_id + 1, id,
property: property.to_string(),
}) })
.await .await
} }
async fn playlist_play_id(&self, id: usize) -> Result<(), MpvError> { async fn unobserve_property(&self, id: usize) -> Result<(), MpvError> {
self.set_property("playlist-pos", id).await self.run_command(MpvCommand::Unobserve(id)).await
} }
async fn playlist_move_id(&self, from: usize, to: usize) -> Result<(), MpvError> { async fn set_playback(&self, option: Switch) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistMove { from, to }) let enabled = match option {
.await Switch::On => "yes",
Switch::Off => "no",
Switch::Toggle => {
self.get_property::<String>("pause")
.await
.map(|s| match s.as_str() {
"yes" => "no",
"no" => "yes",
_ => "no",
})?
}
};
self.set_property("pause", enabled).await
} }
async fn playlist_clear(&self) -> Result<(), MpvError> { async fn prev(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistClear).await self.run_command(MpvCommand::PlaylistPrev).await
}
async fn restart(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Seek {
seconds: 0f64,
option: SeekOptions::Absolute,
})
.await
} }
async fn playlist_add( async fn playlist_add(
@ -247,89 +224,76 @@ impl MpvExt for Mpv {
} }
} }
async fn restart(&self) -> Result<(), MpvError> { async fn playlist_clear(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::Seek { self.run_command(MpvCommand::PlaylistClear).await
seconds: 0f64,
option: SeekOptions::Absolute,
})
.await
} }
async fn prev(&self) -> Result<(), MpvError> { async fn playlist_move_id(&self, from: usize, to: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistPrev).await self.run_command(MpvCommand::PlaylistMove { from, to })
.await
} }
async fn observe_property(&self, id: u64, property: &str) -> Result<(), MpvError> { async fn playlist_play_id(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::Observe { self.set_property("playlist-pos", id).await
id,
property: property.to_string(),
})
.await
} }
async fn unobserve_property(&self, id: u64) -> Result<(), MpvError> { async fn playlist_play_next(&self, id: usize) -> Result<(), MpvError> {
self.run_command(MpvCommand::Unobserve(id)).await match self.get_property::<usize>("playlist-pos").await {
} Ok(current_id) => {
self.run_command(MpvCommand::PlaylistMove {
async fn next(&self) -> Result<(), MpvError> { from: id,
self.run_command(MpvCommand::PlaylistNext).await to: current_id + 1,
} })
.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
} }
NumberChangeOptions::Decrease => { Err(msg) => Err(msg),
self.set_property("volume", volume - input_volume).await
}
NumberChangeOptions::Absolute => self.set_property("volume", input_volume).await,
} }
} }
async fn set_speed( async fn playlist_remove_id(&self, id: usize) -> Result<(), MpvError> {
&self, self.run_command(MpvCommand::PlaylistRemove(id)).await
input_speed: f64,
option: NumberChangeOptions,
) -> Result<(), MpvError> {
let speed = self.get_speed().await?;
match option {
NumberChangeOptions::Increase => self.set_property("speed", speed + input_speed).await,
NumberChangeOptions::Decrease => self.set_property("speed", speed - input_speed).await,
NumberChangeOptions::Absolute => self.set_property("speed", input_speed).await,
}
} }
async fn set_playback(&self, option: Switch) -> Result<(), MpvError> { async fn playlist_shuffle(&self) -> Result<(), MpvError> {
self.run_command(MpvCommand::PlaylistShuffle).await
}
async fn seek(&self, seconds: f64, option: SeekOptions) -> Result<(), MpvError> {
self.run_command(MpvCommand::Seek { seconds, option }).await
}
async fn set_loop_file(&self, option: Switch) -> Result<(), MpvError> {
let enabled = match option { let enabled = match option {
Switch::On => "no", Switch::On => "inf",
Switch::Off => "yes", Switch::Off => "no",
Switch::Toggle => { Switch::Toggle => {
if self.is_playing().await? { self.get_property::<String>("loop-file")
"yes" .await
} else { .map(|s| match s.as_str() {
"no" "inf" => "no",
} "no" => "inf",
_ => "no",
})?
} }
}; };
self.set_property("pause", enabled).await self.set_property("loop-file", enabled).await
}
async fn set_loop_playlist(&self, option: Switch) -> Result<(), MpvError> {
let enabled = match option {
Switch::On => "inf",
Switch::Off => "no",
Switch::Toggle => {
self.get_property::<String>("loop-playlist")
.await
.map(|s| match s.as_str() {
"inf" => "no",
"no" => "inf",
_ => "no",
})?
}
};
self.set_property("loo-playlist", enabled).await
} }
async fn set_mute(&self, option: Switch) -> Result<(), MpvError> { async fn set_mute(&self, option: Switch) -> Result<(), MpvError> {
@ -337,147 +301,61 @@ impl MpvExt for Mpv {
Switch::On => "yes", Switch::On => "yes",
Switch::Off => "no", Switch::Off => "no",
Switch::Toggle => { Switch::Toggle => {
if self.is_muted().await? { self.get_property::<String>("mute")
"no" .await
} else { .map(|s| match s.as_str() {
"yes" "yes" => "no",
} "no" => "yes",
_ => "no",
})?
} }
}; };
self.set_property("mute", enabled).await self.set_property("mute", enabled).await
} }
async fn set_loop_playlist(&self, option: Switch) -> Result<(), MpvError> { async fn set_speed(
let enabled = match option { &self,
Switch::On => "inf", input_speed: f64,
Switch::Off => "no", option: NumberChangeOptions,
Switch::Toggle => match self.playlist_is_looping().await? { ) -> Result<(), MpvError> {
LoopProperty::Inf => "no", match self.get_property::<f64>("speed").await {
LoopProperty::N(_) => "no", Ok(speed) => match option {
LoopProperty::No => "inf", 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,
}, },
}; Err(msg) => Err(msg),
self.set_property("loop-playlist", enabled).await }
} }
async fn set_loop_file(&self, option: Switch) -> Result<(), MpvError> { async fn set_volume(
let enabled = match option { &self,
Switch::On => "inf", input_volume: f64,
Switch::Off => "no", option: NumberChangeOptions,
Switch::Toggle => match self.file_is_looping().await? { ) -> Result<(), MpvError> {
LoopProperty::Inf => "no", match self.get_property::<f64>("volume").await {
LoopProperty::N(_) => "no", Ok(volume) => match option {
LoopProperty::No => "inf", 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,
}, },
}; 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 get_metadata(&self) -> Result<HashMap<String, MpvDataType>, MpvError> { async fn stop(&self) -> Result<(), MpvError> {
let data = self.get_property("metadata").await?; self.run_command(MpvCommand::Stop).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(u64, String), ObserveProperty(usize, String),
UnobserveProperty(u64), UnobserveProperty(usize),
Exit, Exit,
} }
@ -92,7 +92,7 @@ impl MpvIpc {
log::trace!("Received response: {:?}", response); log::trace!("Received response: {:?}", response);
parse_mpv_response_data(response?, command) parse_mpv_response_data(response?)
} }
pub(crate) async fn get_mpv_property( pub(crate) async fn get_mpv_property(
@ -114,14 +114,17 @@ impl MpvIpc {
pub(crate) async fn observe_property( pub(crate) async fn observe_property(
&mut self, &mut self,
id: u64, id: usize,
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(&mut self, id: u64) -> Result<Option<Value>, MpvError> { pub(crate) async fn unobserve_property(
&mut self,
id: usize,
) -> Result<Option<Value>, MpvError> {
self.send_command(&[json!("unobserve_property"), json!(id)]) self.send_command(&[json!("unobserve_property"), json!(id)])
.await .await
} }
@ -194,7 +197,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, command: &[Value]) -> Result<Option<Value>, MpvError> { fn parse_mpv_response_data(value: 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()
@ -221,11 +224,7 @@ fn parse_mpv_response_data(value: Value, command: &[Value]) -> Result<Option<Val
}) })
.and_then(|(error, data)| match error { .and_then(|(error, data)| match error {
"success" => Ok(data), "success" => Ok(data),
"property unavailable" => Ok(None), err => Err(MpvError::MpvError(err.to_string())),
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(),
}) })
.and_then(|array| json_array_to_playlist(array)) .map(|array| json_array_to_playlist(array))
} }
fn as_string(&self) -> String { fn as_string(&self) -> String {
@ -155,65 +155,29 @@ pub(crate) fn json_array_to_vec(array: &[Value]) -> Result<Vec<MpvDataType>, Mpv
array.iter().map(json_to_value).collect() array.iter().map(json_to_value).collect()
} }
fn json_map_to_playlist_entry( pub(crate) fn json_array_to_playlist(array: &[Value]) -> Vec<PlaylistEntry> {
map: &serde_json::map::Map<String, Value>, let mut output: Vec<PlaylistEntry> = Vec::new();
) -> Result<PlaylistEntry, MpvError> { for (id, entry) in array.iter().enumerate() {
let filename = match map.get("filename") { let mut filename: String = String::new();
Some(Value::String(s)) => s.to_string(), let mut title: String = String::new();
Some(data) => { let mut current: bool = false;
return Err(MpvError::ValueContainsUnexpectedType { if let Value::String(ref f) = entry["filename"] {
expected_type: "String".to_owned(), filename = f.to_string();
received: data.clone(),
})
} }
None => return Err(MpvError::MissingMpvData), if let Value::String(ref t) = entry["title"] {
}; 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(),
})
} }
None => None, if let Value::Bool(ref b) = entry["current"] {
}; 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(),
})
} }
None => false, output.push(PlaylistEntry {
}; id,
Ok(PlaylistEntry { filename,
id: 0, title,
filename, current,
title, });
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)]
@ -313,7 +277,7 @@ mod test {
} }
#[test] #[test]
fn test_json_array_to_playlist() -> Result<(), MpvError> { fn test_json_array_to_playlist() {
let json = json!([ let json = json!([
{ {
"filename": "file1", "filename": "file1",
@ -324,10 +288,6 @@ mod test {
"filename": "file2", "filename": "file2",
"title": "title2", "title": "title2",
"current": false "current": false
},
{
"filename": "file3",
"current": false
} }
]); ]);
@ -335,25 +295,17 @@ mod test {
PlaylistEntry { PlaylistEntry {
id: 0, id: 0,
filename: "file1".to_string(), filename: "file1".to_string(),
title: Some("title1".to_string()), title: "title1".to_string(),
current: true, current: true,
}, },
PlaylistEntry { PlaylistEntry {
id: 1, id: 1,
filename: "file2".to_string(), filename: "file2".to_string(),
title: Some("title2".to_string()), title: "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,5 +1,5 @@
use futures::{stream::StreamExt, Stream}; use futures::{stream::StreamExt, Stream};
use mpvipc_async::{parse_property, Event, Mpv, MpvError, MpvExt, Property}; use mpvipc::{parse_event_property, Event, Mpv, MpvError, MpvExt};
use thiserror::Error; use thiserror::Error;
use tokio::time::sleep; use tokio::time::sleep;
use tokio::time::{timeout, Duration}; use tokio::time::{timeout, Duration};
@ -8,22 +8,17 @@ use test_log::test;
use super::*; use super::*;
const MPV_CHANNEL_ID: u64 = 1337; const MPV_CHANNEL_ID: usize = 1337;
#[derive(Error, Debug)] #[derive(Error, Debug)]
enum PropertyCheckingThreadError { enum PropertyCheckingThreadError {
#[error("Unexpected property: {0:?}")] #[error("Unexpected property: {0:?}")]
UnexpectedPropertyError(Property), UnexpectedPropertyError(mpvipc::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,
@ -32,7 +27,7 @@ fn create_interruptable_event_property_checking_thread<T>(
tokio_util::sync::CancellationToken, tokio_util::sync::CancellationToken,
) )
where where
T: Fn(Property) -> bool + Send + 'static, T: Fn(mpvipc::Property) -> bool + Send + 'static,
{ {
let cancellation_token = tokio_util::sync::CancellationToken::new(); let cancellation_token = tokio_util::sync::CancellationToken::new();
let cancellation_token_clone = cancellation_token.clone(); let cancellation_token_clone = cancellation_token.clone();
@ -43,8 +38,8 @@ where
match event { match event {
Some(Ok(event)) => { Some(Ok(event)) => {
match event { match event {
Event::PropertyChange { id: Some(MPV_CHANNEL_ID), name, data } => { Event::PropertyChange { id: MPV_CHANNEL_ID, .. } => {
let property = parse_property(&name, data).unwrap(); let property = parse_event_property(event).unwrap().1;
if !on_property(property.clone()) { if !on_property(property.clone()) {
return Err(PropertyCheckingThreadError::UnexpectedPropertyError(property)) return Err(PropertyCheckingThreadError::UnexpectedPropertyError(property))
} }
@ -66,9 +61,6 @@ 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>>,
@ -111,7 +103,6 @@ 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> {
@ -122,7 +113,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 {
Property::Pause(_) => { mpvipc::Property::Pause(_) => {
log::debug!("{:?}", property); log::debug!("{:?}", property);
true true
} }
@ -140,7 +131,6 @@ 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> {
@ -150,7 +140,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 {
Property::Volume(_) => { mpvipc::Property::Volume(_) => {
log::trace!("{:?}", property); log::trace!("{:?}", property);
true true
} }
@ -170,7 +160,6 @@ 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> {
@ -180,7 +169,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 {
Property::Mute(_) => { mpvipc::Property::Mute(_) => {
log::trace!("{:?}", property); log::trace!("{:?}", property);
true true
} }
@ -198,7 +187,6 @@ 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> {
@ -209,7 +197,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 {
Property::Duration(_) => { mpvipc::Property::Duration(_) => {
log::trace!("{:?}", property); log::trace!("{:?}", property);
true true
} }

View File

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

View File

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