Compare commits
4 Commits
3be7b2bda6
...
bfc27531ef
Author | SHA1 | Date |
---|---|---|
Oystein Tveit | bfc27531ef | |
Oystein Tveit | 559f09dee2 | |
Oystein Tveit | 2056afe002 | |
Oystein Tveit | 60802e1158 |
|
@ -22,7 +22,7 @@ jobs:
|
|||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --all-features --verbose
|
||||
run: cargo build --all-features --verbose --release
|
||||
|
||||
check:
|
||||
runs-on: ubuntu-latest-personal
|
||||
|
@ -49,16 +49,62 @@ jobs:
|
|||
runs-on: ubuntu-latest-personal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Install latest nightly toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
components: rustfmt, clippy, llvm-tools-preview
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Create necessary directories
|
||||
run: mkdir -p target/test-report
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --all-features --verbose
|
||||
run: cargo test --all-features --release -Z unstable-options --report-time --format json | tee target/test-report/test-report.json
|
||||
env:
|
||||
RUSTFLAGS: "-Cinstrument-coverage"
|
||||
LLVM_PROFILE_FILE: "target/release/coverage/%p-%m.profraw"
|
||||
|
||||
- name: Install markdown-test-report
|
||||
run: cargo binstall -y markdown-test-report
|
||||
|
||||
- name: Generate test report
|
||||
run: markdown-test-report target/test-report/test-report.json target/test-report/test-report.md
|
||||
|
||||
- name: Upload test report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-report
|
||||
path: target/test-report/test-report.md
|
||||
|
||||
- name: Install grcov
|
||||
run: cargo binstall -y grcov
|
||||
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
grcov \
|
||||
--source-dir . \
|
||||
--binary-path ./target/release/deps/ \
|
||||
--excl-start 'mod test* \{' \
|
||||
--ignore 'tests/*' \
|
||||
--ignore "*test.rs" \
|
||||
--ignore "*tests.rs" \
|
||||
--ignore "*github.com*" \
|
||||
--ignore "*libcore*" \
|
||||
--ignore "*rustc*" \
|
||||
--ignore "*liballoc*" \
|
||||
--ignore "*cargo*" \
|
||||
-t html \
|
||||
-o ./target/coverage/html \
|
||||
target/coverage/prof
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage
|
||||
path: target/coverage/html
|
||||
|
|
|
@ -22,7 +22,8 @@ tokio-stream = { version = "0.1.15", features = ["sync"] }
|
|||
[dev-dependencies]
|
||||
env_logger = "0.10.0"
|
||||
test-log = "0.2.15"
|
||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "time"] }
|
||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "time", "process"] }
|
||||
uuid = { version = "1.8.0", features = ["v4"] }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
rm -rf target
|
||||
|
||||
mkdir -p target/test-report
|
||||
|
||||
export RUSTFLAGS="-Cinstrument-coverage"
|
||||
export LLVM_PROFILE_FILE="target/coverage/prof/%p-%m.profraw"
|
||||
|
||||
rustup override set nightly
|
||||
|
||||
echo "Running tests..."
|
||||
cargo test --all-features --release --no-fail-fast -- -Z unstable-options --report-time --format json | tee target/test-report/test-report.json
|
||||
|
||||
echo "Generating test report..."
|
||||
markdown-test-report target/test-report/test-report.json --output target/test-report/test-report.md
|
||||
echo "Generating test report HTML..."
|
||||
pandoc target/test-report/test-report.md -o target/test-report/test-report.html
|
||||
|
||||
# rustup override set stable
|
||||
|
||||
echo "Removing unused profraw files..."
|
||||
for file in target/coverage/prof/*.profraw; do
|
||||
~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata show "$file" 1>/dev/null 2>/dev/null || rm -f "$file"
|
||||
done
|
||||
|
||||
echo "Generating coverage report..."
|
||||
grcov \
|
||||
--source-dir . \
|
||||
--binary-path ./target/release/deps/ \
|
||||
--excl-start 'mod test* \{' \
|
||||
--ignore 'tests/*' \
|
||||
--ignore "*test.rs" \
|
||||
--ignore "*tests.rs" \
|
||||
--ignore "*github.com*" \
|
||||
--ignore "*libcore*" \
|
||||
--ignore "*rustc*" \
|
||||
--ignore "*liballoc*" \
|
||||
--ignore "*cargo*" \
|
||||
-t html \
|
||||
-o ./target/coverage/html \
|
||||
target/coverage/prof
|
||||
|
||||
rustup override set nightly
|
|
@ -1,51 +1,30 @@
|
|||
//! The core API for interacting with [`Mpv`].
|
||||
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
use std::{collections::HashMap, fmt};
|
||||
use tokio::{
|
||||
net::UnixStream,
|
||||
sync::{broadcast, mpsc, oneshot},
|
||||
};
|
||||
|
||||
use crate::ipc::{MpvIpc, MpvIpcCommand, MpvIpcEvent, MpvIpcResponse};
|
||||
use crate::message_parser::TypeHandler;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Event {
|
||||
Shutdown,
|
||||
StartFile,
|
||||
EndFile,
|
||||
FileLoaded,
|
||||
TracksChanged,
|
||||
TrackSwitched,
|
||||
Idle,
|
||||
Pause,
|
||||
Unpause,
|
||||
Tick,
|
||||
VideoReconfig,
|
||||
AudioReconfig,
|
||||
MetadataUpdate,
|
||||
Seek,
|
||||
PlaybackRestart,
|
||||
PropertyChange { id: usize, property: Property },
|
||||
ChapterChange,
|
||||
ClientMessage { args: Vec<String> },
|
||||
Unimplemented,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Property {
|
||||
Path(Option<String>),
|
||||
Pause(bool),
|
||||
PlaybackTime(Option<f64>),
|
||||
Duration(Option<f64>),
|
||||
Metadata(Option<HashMap<String, MpvDataType>>),
|
||||
Unknown { name: String, data: MpvDataType },
|
||||
}
|
||||
use crate::{
|
||||
ipc::{MpvIpc, MpvIpcCommand, MpvIpcEvent, MpvIpcResponse},
|
||||
message_parser::TypeHandler,
|
||||
Error, ErrorCode, Event,
|
||||
};
|
||||
|
||||
/// All possible commands that can be sent to mpv.
|
||||
///
|
||||
/// Not all commands are guaranteed to be implemented.
|
||||
/// If something is missing, please open an issue.
|
||||
///
|
||||
/// You can also use the `run_command_raw` function to run commands
|
||||
/// that are not implemented here.
|
||||
///
|
||||
/// See <https://mpv.io/manual/master/#list-of-input-commands> for
|
||||
/// the upstream list of commands.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum MpvCommand {
|
||||
LoadFile {
|
||||
|
@ -83,11 +62,13 @@ pub enum MpvCommand {
|
|||
Unobserve(isize),
|
||||
}
|
||||
|
||||
/// Helper trait to keep track of the string literals that mpv expects.
|
||||
pub(crate) trait IntoRawCommandPart {
|
||||
fn into_raw_command_part(self) -> String;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
/// Generic data type representing all possible data types that mpv can return.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum MpvDataType {
|
||||
Array(Vec<MpvDataType>),
|
||||
Bool(bool),
|
||||
|
@ -99,6 +80,20 @@ pub enum MpvDataType {
|
|||
Usize(usize),
|
||||
}
|
||||
|
||||
/// A mpv playlist.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Playlist(pub Vec<PlaylistEntry>);
|
||||
|
||||
/// A single entry in the mpv playlist.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PlaylistEntry {
|
||||
pub id: usize,
|
||||
pub filename: String,
|
||||
pub title: String,
|
||||
pub current: bool,
|
||||
}
|
||||
|
||||
/// Options for [`MpvCommand::LoadFile`] and [`MpvCommand::LoadList`].
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum PlaylistAddOptions {
|
||||
Replace,
|
||||
|
@ -114,12 +109,7 @@ impl IntoRawCommandPart for PlaylistAddOptions {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum PlaylistAddTypeOptions {
|
||||
File,
|
||||
Playlist,
|
||||
}
|
||||
|
||||
/// Options for [`MpvCommand::Seek`].
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum SeekOptions {
|
||||
Relative,
|
||||
|
@ -139,35 +129,7 @@ impl IntoRawCommandPart for SeekOptions {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ErrorCode {
|
||||
MpvError(String),
|
||||
JsonParseError(String),
|
||||
ConnectError(String),
|
||||
JsonContainsUnexptectedType,
|
||||
UnexpectedResult,
|
||||
UnexpectedValue,
|
||||
MissingValue,
|
||||
UnsupportedType,
|
||||
ValueDoesNotContainBool,
|
||||
ValueDoesNotContainF64,
|
||||
ValueDoesNotContainHashMap,
|
||||
ValueDoesNotContainPlaylist,
|
||||
ValueDoesNotContainString,
|
||||
ValueDoesNotContainUsize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PlaylistEntry {
|
||||
pub id: usize,
|
||||
pub filename: String,
|
||||
pub title: String,
|
||||
pub current: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Playlist(pub Vec<PlaylistEntry>);
|
||||
|
||||
/// A trait for specifying how to extract and parse a value returned through [`Mpv::get_property`].
|
||||
pub trait GetPropertyTypeHandler: Sized {
|
||||
// TODO: fix this
|
||||
#[allow(async_fn_in_trait)]
|
||||
|
@ -186,6 +148,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// A trait for specifying how to serialize and set a value through [`Mpv::set_property`].
|
||||
pub trait SetPropertyTypeHandler<T> {
|
||||
// TODO: fix this
|
||||
#[allow(async_fn_in_trait)]
|
||||
|
@ -220,52 +183,14 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Error(pub ErrorCode);
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl Display for ErrorCode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ErrorCode::ConnectError(ref msg) => f.write_str(&format!("ConnectError: {}", msg)),
|
||||
ErrorCode::JsonParseError(ref msg) => f.write_str(&format!("JsonParseError: {}", msg)),
|
||||
ErrorCode::MpvError(ref msg) => f.write_str(&format!("MpvError: {}", msg)),
|
||||
ErrorCode::JsonContainsUnexptectedType => {
|
||||
f.write_str("Mpv sent a value with an unexpected type")
|
||||
}
|
||||
ErrorCode::UnexpectedResult => f.write_str("Unexpected result received"),
|
||||
ErrorCode::UnexpectedValue => f.write_str("Unexpected value received"),
|
||||
ErrorCode::MissingValue => f.write_str("Missing value"),
|
||||
ErrorCode::UnsupportedType => f.write_str("Unsupported type received"),
|
||||
ErrorCode::ValueDoesNotContainBool => {
|
||||
f.write_str("The received value is not of type \'std::bool\'")
|
||||
}
|
||||
ErrorCode::ValueDoesNotContainF64 => {
|
||||
f.write_str("The received value is not of type \'std::f64\'")
|
||||
}
|
||||
ErrorCode::ValueDoesNotContainHashMap => {
|
||||
f.write_str("The received value is not of type \'std::collections::HashMap\'")
|
||||
}
|
||||
ErrorCode::ValueDoesNotContainPlaylist => {
|
||||
f.write_str("The received value is not of type \'mpvipc::Playlist\'")
|
||||
}
|
||||
ErrorCode::ValueDoesNotContainString => {
|
||||
f.write_str("The received value is not of type \'std::string::String\'")
|
||||
}
|
||||
ErrorCode::ValueDoesNotContainUsize => {
|
||||
f.write_str("The received value is not of type \'std::usize\'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The main struct for interacting with mpv.
|
||||
///
|
||||
/// This struct provides the core API for interacting with mpv.
|
||||
/// These functions are the building blocks for the higher-level API provided by the `MpvExt` trait.
|
||||
/// They can also be used directly to interact with mpv in a more flexible way, mostly returning JSON values.
|
||||
///
|
||||
/// The `Mpv` struct can be cloned freely, and shared anywhere.
|
||||
/// It only contains a message passing channel to the tokio task that handles the IPC communication with mpv.
|
||||
#[derive(Clone)]
|
||||
pub struct Mpv {
|
||||
command_sender: mpsc::Sender<(MpvIpcCommand, oneshot::Sender<MpvIpcResponse>)>,
|
||||
|
@ -323,7 +248,7 @@ impl Mpv {
|
|||
pub async fn get_event_stream(&self) -> impl futures::Stream<Item = Result<Event, Error>> {
|
||||
tokio_stream::wrappers::BroadcastStream::new(self.broadcast_channel.subscribe()).map(
|
||||
|event| match event {
|
||||
Ok(event) => Mpv::map_event(event),
|
||||
Ok(event) => crate::event_parser::parse_event(event),
|
||||
Err(_) => Err(Error(ErrorCode::ConnectError(
|
||||
"Failed to receive event".to_string(),
|
||||
))),
|
||||
|
@ -331,179 +256,46 @@ impl Mpv {
|
|||
)
|
||||
}
|
||||
|
||||
fn map_event(raw_event: MpvIpcEvent) -> Result<Event, Error> {
|
||||
let MpvIpcEvent(event) = raw_event;
|
||||
|
||||
event
|
||||
.as_object()
|
||||
.ok_or(Error(ErrorCode::JsonContainsUnexptectedType))
|
||||
.and_then(|event| {
|
||||
let event_name = event
|
||||
.get("event")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_str()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?;
|
||||
|
||||
match event_name {
|
||||
"shutdown" => Ok(Event::Shutdown),
|
||||
"start-file" => Ok(Event::StartFile),
|
||||
"end-file" => Ok(Event::EndFile),
|
||||
"file-loaded" => Ok(Event::FileLoaded),
|
||||
"tracks-changed" => Ok(Event::TracksChanged),
|
||||
"track-switched" => Ok(Event::TrackSwitched),
|
||||
"idle" => Ok(Event::Idle),
|
||||
"pause" => Ok(Event::Pause),
|
||||
"unpause" => Ok(Event::Unpause),
|
||||
"tick" => Ok(Event::Tick),
|
||||
"video-reconfig" => Ok(Event::VideoReconfig),
|
||||
"audio-reconfig" => Ok(Event::AudioReconfig),
|
||||
"metadata-update" => Ok(Event::MetadataUpdate),
|
||||
"seek" => Ok(Event::Seek),
|
||||
"playback-restart" => Ok(Event::PlaybackRestart),
|
||||
"property-change" => {
|
||||
let id = event
|
||||
.get("id")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_u64()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainUsize))?
|
||||
as usize;
|
||||
let property_name = event
|
||||
.get("name")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_str()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?;
|
||||
|
||||
match property_name {
|
||||
"path" => {
|
||||
let path = event
|
||||
.get("data")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_str()
|
||||
.map(|s| s.to_string());
|
||||
Ok(Event::PropertyChange {
|
||||
id,
|
||||
property: Property::Path(path),
|
||||
})
|
||||
}
|
||||
"pause" => {
|
||||
let pause = event
|
||||
.get("data")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_bool()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainBool))?;
|
||||
Ok(Event::PropertyChange {
|
||||
id,
|
||||
property: Property::Pause(pause),
|
||||
})
|
||||
}
|
||||
// TODO: missing cases
|
||||
_ => {
|
||||
let data = event
|
||||
.get("data")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.clone();
|
||||
Ok(Event::PropertyChange {
|
||||
id,
|
||||
property: Property::Unknown {
|
||||
name: property_name.to_string(),
|
||||
// TODO: fix
|
||||
data: MpvDataType::Double(data.as_f64().unwrap_or(0.0)),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
"chapter-change" => Ok(Event::ChapterChange),
|
||||
"client-message" => {
|
||||
let args = event
|
||||
.get("args")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_array()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
arg.as_str()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainString))
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.collect::<Result<Vec<String>, Error>>()?;
|
||||
Ok(Event::ClientMessage { args })
|
||||
}
|
||||
_ => Ok(Event::Unimplemented),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// # Description
|
||||
///
|
||||
/// Retrieves the property value from mpv.
|
||||
///
|
||||
/// ## Supported types
|
||||
/// - String
|
||||
/// - bool
|
||||
/// - HashMap<String, String> (e.g. for the 'metadata' property)
|
||||
/// - Vec<PlaylistEntry> (for the 'playlist' property)
|
||||
/// - usize
|
||||
/// - f64
|
||||
///
|
||||
/// ## Input arguments
|
||||
///
|
||||
/// - **property** defines the mpv property that should be retrieved
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use mpvipc::{Mpv, Error};
|
||||
/// async fn main() -> Result<(), Error> {
|
||||
/// let mpv = Mpv::connect("/tmp/mpvsocket")?;
|
||||
/// let paused: bool = mpv.get_property("pause").await?;
|
||||
/// let title: String = mpv.get_property("media-title").await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn get_property<T: GetPropertyTypeHandler>(
|
||||
/// Run a custom command.
|
||||
/// This should only be used if the desired command is not implemented
|
||||
/// with [MpvCommand].
|
||||
pub async fn run_command_raw(
|
||||
&self,
|
||||
property: &str,
|
||||
) -> Result<T, Error> {
|
||||
T::get_property_generic(self, property).await
|
||||
}
|
||||
|
||||
/// # Description
|
||||
///
|
||||
/// Retrieves the property value from mpv.
|
||||
/// The result is always of type String, regardless of the type of the value of the mpv property
|
||||
///
|
||||
/// ## Input arguments
|
||||
///
|
||||
/// - **property** defines the mpv property that should be retrieved
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use mpvipc::{Mpv, Error};
|
||||
/// fn main() -> Result<(), Error> {
|
||||
/// let mpv = Mpv::connect("/tmp/mpvsocket")?;
|
||||
/// let title = mpv.get_property_string("media-title")?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn get_property_value(&self, property: &str) -> Result<Value, Error> {
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
) -> Result<Option<Value>, Error> {
|
||||
let command = Vec::from(
|
||||
[command]
|
||||
.iter()
|
||||
.chain(args.iter())
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.as_slice(),
|
||||
);
|
||||
let (res_tx, res_rx) = oneshot::channel();
|
||||
self.command_sender
|
||||
.send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx))
|
||||
.send((MpvIpcCommand::Command(command), res_tx))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
Error(ErrorCode::ConnectError(
|
||||
"Failed to send command".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
match res_rx.await {
|
||||
Ok(MpvIpcResponse(response)) => {
|
||||
response.and_then(|value| value.ok_or(Error(ErrorCode::MissingValue)))
|
||||
}
|
||||
Ok(MpvIpcResponse(response)) => response,
|
||||
Err(err) => Err(Error(ErrorCode::ConnectError(err.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_command_raw_ignore_value(
|
||||
&self,
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
) -> Result<(), Error> {
|
||||
self.run_command_raw(command, args).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// # Description
|
||||
///
|
||||
/// Runs mpv commands. The arguments are passed as a String-Vector reference:
|
||||
|
@ -511,7 +303,7 @@ impl Mpv {
|
|||
/// ## Input arguments
|
||||
///
|
||||
/// - **command** defines the mpv command that should be executed
|
||||
/// - **args** a slice of &str's which define the arguments
|
||||
/// - **args** a slice of `&str`'s which define the arguments
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -630,60 +422,90 @@ impl Mpv {
|
|||
result
|
||||
}
|
||||
|
||||
/// Run a custom command.
|
||||
/// This should only be used if the desired command is not implemented
|
||||
/// with [MpvCommand].
|
||||
pub async fn run_command_raw(
|
||||
/// # Description
|
||||
///
|
||||
/// Retrieves the property value from mpv.
|
||||
///
|
||||
/// ## Supported types
|
||||
/// - `String`
|
||||
/// - `bool`
|
||||
/// - `HashMap<String, String>` (e.g. for the 'metadata' property)
|
||||
/// - `Vec<PlaylistEntry>` (for the 'playlist' property)
|
||||
/// - `usize`
|
||||
/// - `f64`
|
||||
///
|
||||
/// ## Input arguments
|
||||
///
|
||||
/// - **property** defines the mpv property that should be retrieved
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use mpvipc::{Mpv, Error};
|
||||
/// async fn main() -> Result<(), Error> {
|
||||
/// let mpv = Mpv::connect("/tmp/mpvsocket")?;
|
||||
/// let paused: bool = mpv.get_property("pause").await?;
|
||||
/// let title: String = mpv.get_property("media-title").await?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn get_property<T: GetPropertyTypeHandler>(
|
||||
&self,
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
) -> Result<Option<Value>, Error> {
|
||||
let command = Vec::from(
|
||||
[command]
|
||||
.iter()
|
||||
.chain(args.iter())
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.as_slice(),
|
||||
);
|
||||
property: &str,
|
||||
) -> Result<T, Error> {
|
||||
T::get_property_generic(self, property).await
|
||||
}
|
||||
|
||||
/// # Description
|
||||
///
|
||||
/// Retrieves the property value from mpv.
|
||||
/// The result is always of type String, regardless of the type of the value of the mpv property
|
||||
///
|
||||
/// ## Input arguments
|
||||
///
|
||||
/// - **property** defines the mpv property that should be retrieved
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use mpvipc::{Mpv, Error};
|
||||
/// fn main() -> Result<(), Error> {
|
||||
/// let mpv = Mpv::connect("/tmp/mpvsocket")?;
|
||||
/// let title = mpv.get_property_string("media-title")?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
pub async fn get_property_value(&self, property: &str) -> Result<Value, Error> {
|
||||
let (res_tx, res_rx) = oneshot::channel();
|
||||
self.command_sender
|
||||
.send((MpvIpcCommand::Command(command), res_tx))
|
||||
.send((MpvIpcCommand::GetProperty(property.to_owned()), res_tx))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
Error(ErrorCode::ConnectError(
|
||||
"Failed to send command".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
match res_rx.await {
|
||||
Ok(MpvIpcResponse(response)) => response,
|
||||
Ok(MpvIpcResponse(response)) => {
|
||||
response.and_then(|value| value.ok_or(Error(ErrorCode::MissingValue)))
|
||||
}
|
||||
Err(err) => Err(Error(ErrorCode::ConnectError(err.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_command_raw_ignore_value(
|
||||
&self,
|
||||
command: &str,
|
||||
args: &[&str],
|
||||
) -> Result<(), Error> {
|
||||
self.run_command_raw(command, args).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// # Description
|
||||
///
|
||||
/// Sets the mpv property _<property>_ to _<value>_.
|
||||
/// Sets the mpv property _`<property>`_ to _`<value>`_.
|
||||
///
|
||||
/// ## Supported types
|
||||
/// - String
|
||||
/// - bool
|
||||
/// - f64
|
||||
/// - usize
|
||||
/// - `String`
|
||||
/// - `bool`
|
||||
/// - `f64`
|
||||
/// - `usize`
|
||||
///
|
||||
/// ## Input arguments
|
||||
///
|
||||
/// - **property** defines the mpv property that should be retrieved
|
||||
/// - **value** defines the value of the given mpv property _<property>_
|
||||
/// - **value** defines the value of the given mpv property _`<property>`_
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
|
@ -0,0 +1,72 @@
|
|||
//! Library specific error messages.
|
||||
|
||||
use core::fmt;
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// All possible errors that can occur when interacting with mpv.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ErrorCode {
|
||||
MpvError(String),
|
||||
JsonParseError(String),
|
||||
ConnectError(String),
|
||||
JsonContainsUnexptectedType,
|
||||
UnexpectedResult,
|
||||
UnexpectedValue,
|
||||
MissingValue,
|
||||
UnsupportedType,
|
||||
ValueDoesNotContainBool,
|
||||
ValueDoesNotContainF64,
|
||||
ValueDoesNotContainHashMap,
|
||||
ValueDoesNotContainPlaylist,
|
||||
ValueDoesNotContainString,
|
||||
ValueDoesNotContainUsize,
|
||||
}
|
||||
|
||||
/// Any error that can occur when interacting with mpv.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Error(pub ErrorCode);
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl Display for ErrorCode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ErrorCode::ConnectError(ref msg) => f.write_str(&format!("ConnectError: {}", msg)),
|
||||
ErrorCode::JsonParseError(ref msg) => f.write_str(&format!("JsonParseError: {}", msg)),
|
||||
ErrorCode::MpvError(ref msg) => f.write_str(&format!("MpvError: {}", msg)),
|
||||
ErrorCode::JsonContainsUnexptectedType => {
|
||||
f.write_str("Mpv sent a value with an unexpected type")
|
||||
}
|
||||
ErrorCode::UnexpectedResult => f.write_str("Unexpected result received"),
|
||||
ErrorCode::UnexpectedValue => f.write_str("Unexpected value received"),
|
||||
ErrorCode::MissingValue => f.write_str("Missing value"),
|
||||
ErrorCode::UnsupportedType => f.write_str("Unsupported type received"),
|
||||
ErrorCode::ValueDoesNotContainBool => {
|
||||
f.write_str("The received value is not of type \'std::bool\'")
|
||||
}
|
||||
ErrorCode::ValueDoesNotContainF64 => {
|
||||
f.write_str("The received value is not of type \'std::f64\'")
|
||||
}
|
||||
ErrorCode::ValueDoesNotContainHashMap => {
|
||||
f.write_str("The received value is not of type \'std::collections::HashMap\'")
|
||||
}
|
||||
ErrorCode::ValueDoesNotContainPlaylist => {
|
||||
f.write_str("The received value is not of type \'mpvipc::Playlist\'")
|
||||
}
|
||||
ErrorCode::ValueDoesNotContainString => {
|
||||
f.write_str("The received value is not of type \'std::string::String\'")
|
||||
}
|
||||
ErrorCode::ValueDoesNotContainUsize => {
|
||||
f.write_str("The received value is not of type \'std::usize\'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
//! JSON parsing logic for events from [`MpvIpc`](crate::ipc::MpvIpc).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::{ipc::MpvIpcEvent, Error, ErrorCode, MpvDataType};
|
||||
|
||||
/// All possible properties that can be observed through the event system.
|
||||
///
|
||||
/// Not all properties are guaranteed to be implemented.
|
||||
/// If something is missing, please open an issue.
|
||||
///
|
||||
/// Otherwise, the property will be returned as a `Property::Unknown` variant.
|
||||
///
|
||||
/// See <https://mpv.io/manual/master/#properties> for
|
||||
/// the upstream list of properties.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Property {
|
||||
Path(Option<String>),
|
||||
Pause(bool),
|
||||
PlaybackTime(Option<f64>),
|
||||
Duration(Option<f64>),
|
||||
Metadata(Option<HashMap<String, MpvDataType>>),
|
||||
Unknown { name: String, data: MpvDataType },
|
||||
}
|
||||
|
||||
/// All possible events that can be sent by mpv.
|
||||
///
|
||||
/// Not all event types are guaranteed to be implemented.
|
||||
/// If something is missing, please open an issue.
|
||||
///
|
||||
/// Otherwise, the event will be returned as an `Event::Unimplemented` variant.
|
||||
///
|
||||
/// See <https://mpv.io/manual/master/#list-of-events> for
|
||||
/// the upstream list of events.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Event {
|
||||
Shutdown,
|
||||
StartFile,
|
||||
EndFile,
|
||||
FileLoaded,
|
||||
TracksChanged,
|
||||
TrackSwitched,
|
||||
Idle,
|
||||
Pause,
|
||||
Unpause,
|
||||
Tick,
|
||||
VideoReconfig,
|
||||
AudioReconfig,
|
||||
MetadataUpdate,
|
||||
Seek,
|
||||
PlaybackRestart,
|
||||
PropertyChange { id: usize, property: Property },
|
||||
ChapterChange,
|
||||
ClientMessage { args: Vec<String> },
|
||||
Unimplemented,
|
||||
}
|
||||
|
||||
/// Parse a highlevel [`Event`] objects from json.
|
||||
pub(crate) fn parse_event(raw_event: MpvIpcEvent) -> Result<Event, Error> {
|
||||
let MpvIpcEvent(event) = raw_event;
|
||||
|
||||
event
|
||||
.as_object()
|
||||
.ok_or(Error(ErrorCode::JsonContainsUnexptectedType))
|
||||
.and_then(|event| {
|
||||
let event_name = event
|
||||
.get("event")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_str()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?;
|
||||
|
||||
match event_name {
|
||||
"shutdown" => Ok(Event::Shutdown),
|
||||
"start-file" => Ok(Event::StartFile),
|
||||
"end-file" => Ok(Event::EndFile),
|
||||
"file-loaded" => Ok(Event::FileLoaded),
|
||||
"tracks-changed" => Ok(Event::TracksChanged),
|
||||
"track-switched" => Ok(Event::TrackSwitched),
|
||||
"idle" => Ok(Event::Idle),
|
||||
"pause" => Ok(Event::Pause),
|
||||
"unpause" => Ok(Event::Unpause),
|
||||
"tick" => Ok(Event::Tick),
|
||||
"video-reconfig" => Ok(Event::VideoReconfig),
|
||||
"audio-reconfig" => Ok(Event::AudioReconfig),
|
||||
"metadata-update" => Ok(Event::MetadataUpdate),
|
||||
"seek" => Ok(Event::Seek),
|
||||
"playback-restart" => Ok(Event::PlaybackRestart),
|
||||
"property-change" => parse_event_property(event)
|
||||
.and_then(|(id, property)| Ok(Event::PropertyChange { id, property })),
|
||||
"chapter-change" => Ok(Event::ChapterChange),
|
||||
"client-message" => {
|
||||
let args = event
|
||||
.get("args")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_array()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
arg.as_str()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainString))
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.collect::<Result<Vec<String>, Error>>()?;
|
||||
Ok(Event::ClientMessage { args })
|
||||
}
|
||||
_ => Ok(Event::Unimplemented),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a highlevel [`Property`] object from json, used for [`Event::PropertyChange`].
|
||||
fn parse_event_property(event: &Map<String, Value>) -> Result<(usize, Property), Error> {
|
||||
let id = event
|
||||
.get("id")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_u64()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainUsize))? as usize;
|
||||
let property_name = event
|
||||
.get("name")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_str()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainString))?;
|
||||
|
||||
match property_name {
|
||||
"path" => {
|
||||
let path = event
|
||||
.get("data")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_str()
|
||||
.map(|s| s.to_string());
|
||||
Ok((id, Property::Path(path)))
|
||||
}
|
||||
"pause" => {
|
||||
let pause = event
|
||||
.get("data")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.as_bool()
|
||||
.ok_or(Error(ErrorCode::ValueDoesNotContainBool))?;
|
||||
Ok((id, Property::Pause(pause)))
|
||||
}
|
||||
// TODO: missing cases
|
||||
_ => {
|
||||
let data = event
|
||||
.get("data")
|
||||
.ok_or(Error(ErrorCode::MissingValue))?
|
||||
.clone();
|
||||
Ok((
|
||||
id,
|
||||
Property::Unknown {
|
||||
name: property_name.to_string(),
|
||||
// TODO: fix
|
||||
data: MpvDataType::Double(data.as_f64().unwrap_or(0.0)),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
//! High-level API extension for [`Mpv`].
|
||||
|
||||
use crate::{
|
||||
Error, IntoRawCommandPart, Mpv, MpvCommand, MpvDataType, Playlist, PlaylistAddOptions,
|
||||
PlaylistAddTypeOptions, PlaylistEntry, SeekOptions,
|
||||
PlaylistEntry, SeekOptions,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Generic high-level command for changing a number property.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum NumberChangeOptions {
|
||||
Absolute,
|
||||
|
@ -22,6 +25,7 @@ impl IntoRawCommandPart for NumberChangeOptions {
|
|||
}
|
||||
}
|
||||
|
||||
/// Generic high-level switch for toggling boolean properties.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum Switch {
|
||||
On,
|
||||
|
@ -29,6 +33,14 @@ pub enum Switch {
|
|||
Toggle,
|
||||
}
|
||||
|
||||
/// Options for [`MpvExt::playlist_add`].
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum PlaylistAddTypeOptions {
|
||||
File,
|
||||
Playlist,
|
||||
}
|
||||
|
||||
/// A set of typesafe high-level functions to interact with [`Mpv`].
|
||||
// TODO: fix this
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait MpvExt {
|
28
src/ipc.rs
28
src/ipc.rs
|
@ -1,19 +1,28 @@
|
|||
use super::*;
|
||||
//! IPC handling thread/task. Handles communication between [`Mpv`](crate::Mpv) instances and mpv's unix socket
|
||||
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use serde_json::{json, Value};
|
||||
use std::mem;
|
||||
use tokio::net::UnixStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::{broadcast, oneshot, Mutex};
|
||||
use tokio::{
|
||||
net::UnixStream,
|
||||
sync::{broadcast, mpsc, oneshot, Mutex},
|
||||
};
|
||||
use tokio_util::codec::{Framed, LinesCodec, LinesCodecError};
|
||||
|
||||
use crate::{Error, ErrorCode};
|
||||
|
||||
/// Container for all state that regards communication with the mpv IPC socket
|
||||
/// and message passing with [`Mpv`](crate::Mpv) controllers.
|
||||
pub(crate) struct MpvIpc {
|
||||
socket: Framed<UnixStream, LinesCodec>,
|
||||
command_channel: mpsc::Receiver<(MpvIpcCommand, oneshot::Sender<MpvIpcResponse>)>,
|
||||
// I had trouble with reading and writing to the socket when it was wrapped
|
||||
// in a MutexGuard, so I'm using a separate Mutex to lock the socket when needed.
|
||||
socket_lock: Mutex<()>,
|
||||
command_channel: mpsc::Receiver<(MpvIpcCommand, oneshot::Sender<MpvIpcResponse>)>,
|
||||
event_channel: broadcast::Sender<MpvIpcEvent>,
|
||||
}
|
||||
|
||||
/// Commands that can be sent to [`MpvIpc`]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum MpvIpcCommand {
|
||||
Command(Vec<String>),
|
||||
|
@ -24,9 +33,11 @@ pub(crate) enum MpvIpcCommand {
|
|||
Exit,
|
||||
}
|
||||
|
||||
/// [`MpvIpc`]'s response to a [`MpvIpcCommand`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MpvIpcResponse(pub(crate) Result<Option<Value>, Error>);
|
||||
|
||||
/// A deserialized and partially parsed event from mpv.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MpvIpcEvent(pub(crate) Value);
|
||||
|
||||
|
@ -88,10 +99,6 @@ impl MpvIpc {
|
|||
property: &str,
|
||||
value: Value,
|
||||
) -> Result<Option<Value>, Error> {
|
||||
// let str_value = match &value {
|
||||
// Value::String(s) => s,
|
||||
// v => &serde_json::to_string(&v).unwrap(),
|
||||
// };
|
||||
self.send_command(&[json!("set_property"), json!(property), value])
|
||||
.await
|
||||
}
|
||||
|
@ -177,6 +184,9 @@ impl MpvIpc {
|
|||
}
|
||||
}
|
||||
|
||||
/// This function does the most basic JSON parsing and error handling
|
||||
/// for status codes and errors that all responses from mpv are
|
||||
/// expected to contain.
|
||||
fn parse_mpv_response_data(value: Value) -> Result<Option<Value>, Error> {
|
||||
log::trace!("Parsing mpv response data: {:?}", value);
|
||||
let result = value
|
||||
|
|
14
src/lib.rs
14
src/lib.rs
|
@ -1,7 +1,13 @@
|
|||
mod api;
|
||||
mod api_extension;
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
mod core_api;
|
||||
mod error;
|
||||
mod event_parser;
|
||||
mod highlevel_api_extension;
|
||||
mod ipc;
|
||||
mod message_parser;
|
||||
|
||||
pub use api::*;
|
||||
pub use api_extension::*;
|
||||
pub use core_api::*;
|
||||
pub use error::*;
|
||||
pub use event_parser::*;
|
||||
pub use highlevel_api_extension::*;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! JSON parsing logic for command responses from [`MpvIpc`](crate::ipc::MpvIpc).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
@ -134,61 +136,27 @@ pub(crate) fn json_map_to_hashmap(
|
|||
}
|
||||
|
||||
pub(crate) fn json_array_to_vec(array: &[Value]) -> Vec<MpvDataType> {
|
||||
let mut output: Vec<MpvDataType> = Vec::new();
|
||||
if !array.is_empty() {
|
||||
match array[0] {
|
||||
Value::Array(_) => {
|
||||
for entry in array {
|
||||
if let Value::Array(ref a) = *entry {
|
||||
output.push(MpvDataType::Array(json_array_to_vec(a)));
|
||||
}
|
||||
array
|
||||
.into_iter()
|
||||
.map(|entry| match entry {
|
||||
Value::Array(a) => MpvDataType::Array(json_array_to_vec(&a)),
|
||||
Value::Bool(b) => MpvDataType::Bool(*b),
|
||||
Value::Number(n) => {
|
||||
if n.is_u64() {
|
||||
MpvDataType::Usize(n.as_u64().unwrap() as usize)
|
||||
} else if n.is_f64() {
|
||||
MpvDataType::Double(n.as_f64().unwrap())
|
||||
} else {
|
||||
panic!("unimplemented number");
|
||||
}
|
||||
}
|
||||
|
||||
Value::Bool(_) => {
|
||||
for entry in array {
|
||||
if let Value::Bool(ref b) = *entry {
|
||||
output.push(MpvDataType::Bool(*b));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Value::Number(_) => {
|
||||
for entry in array {
|
||||
if let Value::Number(ref n) = *entry {
|
||||
if n.is_u64() {
|
||||
output.push(MpvDataType::Usize(n.as_u64().unwrap() as usize));
|
||||
} else if n.is_f64() {
|
||||
output.push(MpvDataType::Double(n.as_f64().unwrap()));
|
||||
} else {
|
||||
panic!("unimplemented number");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Value::Object(_) => {
|
||||
for entry in array {
|
||||
if let Value::Object(ref map) = *entry {
|
||||
output.push(MpvDataType::HashMap(json_map_to_hashmap(map)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Value::String(_) => {
|
||||
for entry in array {
|
||||
if let Value::String(ref s) = *entry {
|
||||
output.push(MpvDataType::String(s.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Value::Object(o) => MpvDataType::HashMap(json_map_to_hashmap(&o)),
|
||||
Value::String(s) => MpvDataType::String(s.to_owned()),
|
||||
Value::Null => {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn json_array_to_playlist(array: &[Value]) -> Vec<PlaylistEntry> {
|
||||
|
@ -215,3 +183,137 @@ pub(crate) fn json_array_to_playlist(array: &[Value]) -> Vec<PlaylistEntry> {
|
|||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::MpvDataType;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_json_map_to_hashmap() {
|
||||
let json = json!({
|
||||
"array": [1, 2, 3],
|
||||
"bool": true,
|
||||
"double": 1.0,
|
||||
"usize": 1,
|
||||
"string": "string",
|
||||
"object": {
|
||||
"key": "value"
|
||||
}
|
||||
});
|
||||
|
||||
let mut expected = HashMap::new();
|
||||
expected.insert(
|
||||
"array".to_string(),
|
||||
MpvDataType::Array(vec![
|
||||
MpvDataType::Usize(1),
|
||||
MpvDataType::Usize(2),
|
||||
MpvDataType::Usize(3),
|
||||
]),
|
||||
);
|
||||
expected.insert("bool".to_string(), MpvDataType::Bool(true));
|
||||
expected.insert("double".to_string(), MpvDataType::Double(1.0));
|
||||
expected.insert("usize".to_string(), MpvDataType::Usize(1));
|
||||
expected.insert(
|
||||
"string".to_string(),
|
||||
MpvDataType::String("string".to_string()),
|
||||
);
|
||||
expected.insert(
|
||||
"object".to_string(),
|
||||
MpvDataType::HashMap(HashMap::from([(
|
||||
"key".to_string(),
|
||||
MpvDataType::String("value".to_string()),
|
||||
)])),
|
||||
);
|
||||
|
||||
assert_eq!(json_map_to_hashmap(&json.as_object().unwrap()), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_json_map_to_hashmap_fail_on_null() {
|
||||
json_map_to_hashmap(
|
||||
json!({
|
||||
"null": null
|
||||
})
|
||||
.as_object()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_array_to_vec() {
|
||||
let json = json!([
|
||||
[1, 2, 3],
|
||||
true,
|
||||
1.0,
|
||||
1,
|
||||
"string",
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
]);
|
||||
|
||||
println!("{:?}", json.as_array().unwrap());
|
||||
println!("{:?}", json_array_to_vec(&json.as_array().unwrap()));
|
||||
|
||||
let expected = vec![
|
||||
MpvDataType::Array(vec![
|
||||
MpvDataType::Usize(1),
|
||||
MpvDataType::Usize(2),
|
||||
MpvDataType::Usize(3),
|
||||
]),
|
||||
MpvDataType::Bool(true),
|
||||
MpvDataType::Double(1.0),
|
||||
MpvDataType::Usize(1),
|
||||
MpvDataType::String("string".to_string()),
|
||||
MpvDataType::HashMap(HashMap::from([(
|
||||
"key".to_string(),
|
||||
MpvDataType::String("value".to_string()),
|
||||
)])),
|
||||
];
|
||||
|
||||
assert_eq!(json_array_to_vec(&json.as_array().unwrap()), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_json_array_to_vec_fail_on_null() {
|
||||
json_array_to_vec(json!([null]).as_array().unwrap().as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_array_to_playlist() {
|
||||
let json = json!([
|
||||
{
|
||||
"filename": "file1",
|
||||
"title": "title1",
|
||||
"current": true
|
||||
},
|
||||
{
|
||||
"filename": "file2",
|
||||
"title": "title2",
|
||||
"current": false
|
||||
}
|
||||
]);
|
||||
|
||||
let expected = vec![
|
||||
PlaylistEntry {
|
||||
id: 0,
|
||||
filename: "file1".to_string(),
|
||||
title: "title1".to_string(),
|
||||
current: true,
|
||||
},
|
||||
PlaylistEntry {
|
||||
id: 1,
|
||||
filename: "file2".to_string(),
|
||||
title: "title2".to_string(),
|
||||
current: false,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(json_array_to_playlist(&json.as_array().unwrap()), expected);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
use mpvipc::{Error, Mpv, MpvExt};
|
||||
use std::path::Path;
|
||||
use tokio::{
|
||||
process::{Child, Command},
|
||||
time::{sleep, timeout, Duration},
|
||||
};
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
async fn spawn_headless_mpv() -> Result<(Child, Mpv), Error> {
|
||||
let socket_path_str = format!("/tmp/mpv-ipc-{}", uuid::Uuid::new_v4());
|
||||
let socket_path = Path::new(&socket_path_str);
|
||||
|
||||
let process_handle = Command::new("mpv")
|
||||
.arg("--no-config")
|
||||
.arg("--idle")
|
||||
.arg("--no-video")
|
||||
.arg("--no-audio")
|
||||
.arg(format!(
|
||||
"--input-ipc-server={}",
|
||||
&socket_path.to_str().unwrap()
|
||||
))
|
||||
.spawn()
|
||||
.expect("Failed to start mpv");
|
||||
|
||||
if timeout(Duration::from_millis(500), async {
|
||||
while !&socket_path.exists() {
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
panic!("Failed to create mpv socket at {:?}", &socket_path);
|
||||
}
|
||||
|
||||
let mpv = Mpv::connect(socket_path.to_str().unwrap()).await.unwrap();
|
||||
Ok((process_handle, mpv))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(target_family = "unix")]
|
||||
async fn test_get_mpv_version() {
|
||||
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
|
||||
let version: String = mpv.get_property("mpv-version").await.unwrap();
|
||||
assert!(version.starts_with("mpv"));
|
||||
|
||||
mpv.kill().await.unwrap();
|
||||
proc.kill().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(target_family = "unix")]
|
||||
async fn test_set_property() {
|
||||
let (mut proc, mpv) = spawn_headless_mpv().await.unwrap();
|
||||
mpv.set_property("pause", true).await.unwrap();
|
||||
let paused: bool = mpv.get_property("pause").await.unwrap();
|
||||
assert_eq!(paused, true);
|
||||
|
||||
mpv.kill().await.unwrap();
|
||||
proc.kill().await.unwrap();
|
||||
}
|
Loading…
Reference in New Issue