#![warn(clippy::all, clippy::unwrap_used)]
#![allow(clippy::missing_errors_doc)]
use app_control::AppControlService;
use audio::AudioService;
use av_content::AvContentService;
use derive_builder::Builder;
use encryption::EncryptionService;
use error::{Error, Result};
use guide::GuideService;
use reqwest::{header::CONTENT_TYPE, Client, StatusCode};
use serde::Serialize;
use serde_json::Value;
use std::collections::HashMap;
use system::SystemService;
use video::VideoService;
use video_screen::VideoScreenService;
pub mod app_control;
pub mod audio;
pub mod av_content;
pub mod encryption;
pub mod error;
pub mod guide;
pub mod system;
pub mod video;
pub mod video_screen;
type VersionsVec = Vec<String>;
type APIsMap = HashMap<String, VersionsVec>;
type ServicesMap = HashMap<String, APIsMap>;
#[derive(Serialize, Builder, Clone, Default)]
#[builder(build_fn(error = "derive_builder::UninitializedFieldError"))]
struct RequestBody<'a> {
id: usize,
method: &'a str,
#[builder(setter(custom), default = "\"1.0\"")]
version: &'a str,
#[builder(setter(custom), default)]
params: Vec<Value>,
}
impl<'a> RequestBodyBuilder<'a> {
fn version(&mut self, value: Option<&'a str>) -> &mut Self {
self.version = if value.is_none() { Some("1.0") } else { value };
self
}
fn params(&mut self, values: Value) -> &mut Self {
self.params = Some(vec![values]);
self
}
}
#[derive(Clone)]
enum RequestGetElementType<'a> {
Index(usize),
Text(&'a str),
}
impl<'a> From<&'a str> for RequestGetElementType<'a> {
fn from(value: &'a str) -> Self {
Self::Text(value)
}
}
impl<'a> From<usize> for RequestGetElementType<'a> {
fn from(value: usize) -> Self {
Self::Index(value)
}
}
#[derive(Builder)]
#[builder(build_fn(error = "derive_builder::UninitializedFieldError"))]
struct Request<'a> {
endpoint: &'a str,
body: RequestBody<'a>,
#[builder(setter(custom), default)]
is_protected: bool,
#[builder(setter(custom), default)]
has_result: bool,
#[builder(default = "RequestGetElementType::Index(0)")]
get: RequestGetElementType<'a>,
}
impl<'a> RequestBuilder<'a> {
fn is_protected(&mut self) -> &mut Self {
self.is_protected = Some(true);
self
}
fn has_result(&mut self) -> &mut Self {
self.has_result = Some(true);
self
}
#[allow(clippy::unwrap_used)]
async fn make(&mut self, bravia: &Bravia) -> Result<Value> {
let request = self.build()?;
Ok(bravia.make_request(request).await.unwrap())
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct Bravia {
base_url: String,
auth: Option<String>,
api_support: ServicesMap,
}
impl Bravia {
pub async fn new(address: &str, auth: Option<&str>) -> Result<Self> {
let mut ret = Bravia {
base_url: format!("{address}/sony/"),
auth: auth.map(str::to_string),
api_support: HashMap::new(),
};
ret.create_supported_api_cache().await?;
Ok(ret)
}
pub fn guide(&self) -> GuideService {
GuideService::new(self)
}
pub fn app_control(&self) -> AppControlService {
AppControlService::new(self)
}
pub fn audio(&self) -> AudioService {
AudioService::new(self)
}
pub fn av_content(&self) -> AvContentService {
AvContentService::new(self)
}
pub fn encryption(&self) -> EncryptionService {
EncryptionService::new(self)
}
pub fn system(&self) -> SystemService {
SystemService::new(self)
}
pub fn video(&self) -> VideoService {
VideoService::new(self)
}
pub fn video_screen(&self) -> VideoScreenService {
VideoScreenService::new(self)
}
async fn create_supported_api_cache(&mut self) -> Result<()> {
let services = self.guide().get_supported_api_info(None).await?;
for service in services {
let mut service_apis = HashMap::new();
for api in service.apis {
let api_versions: Vec<String> =
api.versions.iter().map(|x| x.version.to_string()).collect();
service_apis.insert(api.name.to_string(), api_versions);
}
self.api_support.insert(service.service, service_apis);
}
Ok(())
}
fn is_api_supported(&self, service: &str, api: &str, api_level: &str) -> Result<()> {
if let Some(service) = self.api_support.get(service) {
if let Some(api) = service.get(api) {
if api.iter().any(|x| x == api_level) {
Ok(())
} else {
Err(Error::BraviaApiLevelError)
}
} else {
Err(Error::BraviaApiNotFound)
}
} else {
Err(Error::BraviaApiServiceNotFound)
}
}
async fn make_request<'a>(&self, req: Request<'a>) -> Result<Value> {
let url = format!("{}{}", self.base_url, req.endpoint);
if req.body.method != "getSupportedApiInfo" {
self.is_api_supported(req.endpoint, req.body.method, req.body.version)?;
};
let auth: &str = if req.is_protected {
if let Some(value) = &self.auth {
value.as_str()
} else {
return Err(Error::BraviaAuthLevelError);
}
} else {
""
};
let resp = Client::new()
.post(url)
.header("X-Auth-PSK", auth)
.header(CONTENT_TYPE, "application/json")
.body(serde_json::to_string(&req.body)?)
.send()
.await;
match resp {
Ok(resp) => {
match resp.status() {
StatusCode::OK => {
let mut parsed = resp.json::<serde_json::Value>().await?;
if let Some(result) = parsed.get_mut("result") {
if req.has_result {
let result = match req.get {
RequestGetElementType::Index(value) => result.get_mut(value),
RequestGetElementType::Text(value) => result[0].get_mut(value),
};
Ok(result.ok_or(Error::MissingValue("result values"))?.take())
} else {
Ok(serde_json::Value::Null)
}
} else if let Some(error) = parsed.get_mut("error") {
let api_error = error.take();
let err = serde_json::from_value(api_error)?;
Err(Error::BraviaError(err))
} else {
Err(Error::InvalidResponse("Missing result and error fields."))
}
}
_ => Err(Error::BadStatus(resp.status())),
}
}
Err(err) => Err(Error::NetworkError(err)),
}
}
}