use serde::{Deserialize, Serialize};
use crate::layout::Layout;
use crate::plugin::BackendKind;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum Request {
Ping,
Shutdown,
SetPersistent {
persistent: bool,
},
List,
Open {
url: String,
start: Option<f64>,
paused: bool,
subtitles: Option<String>,
window: WindowOpts,
backend: Option<BackendKind>,
},
Player {
id: String,
action: PlayerAction,
},
Window {
id: String,
opts: WindowOpts,
},
Close {
id: String,
},
Eval {
id: String,
script: String,
},
Screenshot {
id: String,
path: String,
},
AuthLogin {
service: String,
},
AuthStatus {
service: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum PlayerAction {
Status,
Play,
Pause,
Toggle,
Next,
Previous,
Seek {
seconds: f64,
relative: bool,
},
Subtitles {
lang: String,
},
Audio {
lang: String,
},
Volume {
level: Option<u8>,
muted: Option<bool>,
},
Speed {
rate: f64,
},
Quality {
level: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, clap::ValueEnum)]
#[serde(rename_all = "snake_case")]
pub enum Switch {
On,
Off,
Toggle,
}
impl Switch {
pub fn resolve(self, current: bool) -> bool {
match self {
Switch::On => true,
Switch::Off => false,
Switch::Toggle => !current,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WindowOpts {
pub size: Option<(f64, f64)>,
pub position: Option<(f64, f64)>,
pub layout: Option<Layout>,
pub fullscreen: Option<Switch>,
pub on_top: Option<Switch>,
}
impl WindowOpts {
pub fn changes_geometry(&self) -> bool {
self.size.is_some() || self.position.is_some() || self.layout.is_some()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Response {
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl From<Result<serde_json::Value, String>> for Response {
fn from(r: Result<serde_json::Value, String>) -> Self {
match r {
Ok(v) => Response {
ok: true,
result: Some(v),
error: None,
},
Err(e) => Response {
ok: false,
result: None,
error: Some(e),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn player_actions_match_the_plugin_contract() {
let cases = [
(PlayerAction::Status, json!({ "action": "status" })),
(PlayerAction::Play, json!({ "action": "play" })),
(PlayerAction::Pause, json!({ "action": "pause" })),
(PlayerAction::Toggle, json!({ "action": "toggle" })),
(PlayerAction::Next, json!({ "action": "next" })),
(PlayerAction::Previous, json!({ "action": "previous" })),
(
PlayerAction::Seek {
seconds: -5.0,
relative: true,
},
json!({ "action": "seek", "seconds": -5.0, "relative": true }),
),
(
PlayerAction::Subtitles { lang: "ja".into() },
json!({ "action": "subtitles", "lang": "ja" }),
),
(
PlayerAction::Audio {
lang: "list".into(),
},
json!({ "action": "audio", "lang": "list" }),
),
(
PlayerAction::Volume {
level: Some(5),
muted: None,
},
json!({ "action": "volume", "level": 5, "muted": null }),
),
(
PlayerAction::Speed { rate: 2.0 },
json!({ "action": "speed", "rate": 2.0 }),
),
(
PlayerAction::Quality {
level: "hd720".into(),
},
json!({ "action": "quality", "level": "hd720" }),
),
];
for (action, expected) in cases {
let v = serde_json::to_value(&action).unwrap();
assert_eq!(v, expected);
let back: PlayerAction = serde_json::from_value(v.clone()).unwrap();
assert_eq!(serde_json::to_value(back).unwrap(), v);
}
}
#[test]
fn requests_round_trip() {
let reqs = [
Request::Ping,
Request::Shutdown,
Request::List,
Request::Close { id: "1".into() },
Request::Screenshot {
id: "1".into(),
path: "/tmp/x.png".into(),
},
Request::AuthLogin {
service: "youtube".into(),
},
Request::AuthStatus { service: None },
Request::Window {
id: "1".into(),
opts: WindowOpts {
layout: Some(Layout::TopRight),
..Default::default()
},
},
];
for r in reqs {
let v = serde_json::to_value(&r).unwrap();
let back: Request = serde_json::from_value(v.clone()).unwrap();
assert_eq!(serde_json::to_value(back).unwrap(), v);
}
assert!(serde_json::from_str::<Request>("{\"cmd\":\"explode\"}").is_err());
}
#[test]
fn responses() {
let ok = serde_json::to_value(Response::from(Ok(json!(1)))).unwrap();
assert_eq!(ok, json!({ "ok": true, "result": 1 }));
let err = serde_json::to_value(Response::from(Err("bad".to_string()))).unwrap();
assert_eq!(err, json!({ "ok": false, "error": "bad" }));
}
#[test]
fn switches_and_geometry_flags() {
assert!(Switch::On.resolve(false));
assert!(!Switch::Off.resolve(true));
assert!(Switch::Toggle.resolve(false));
assert!(!Switch::Toggle.resolve(true));
assert!(!WindowOpts::default().changes_geometry());
assert!(
!WindowOpts {
fullscreen: Some(Switch::On),
on_top: Some(Switch::On),
..Default::default()
}
.changes_geometry()
);
assert!(
WindowOpts {
size: Some((1.0, 1.0)),
..Default::default()
}
.changes_geometry()
);
assert!(
WindowOpts {
position: Some((1.0, 1.0)),
..Default::default()
}
.changes_geometry()
);
assert!(
WindowOpts {
layout: Some(Layout::Left),
..Default::default()
}
.changes_geometry()
);
}
}