use futures::prelude::*;
use reqwest::{Client, Url};
use serde::Deserialize;
use serde_xml_rs::from_str;
use ssdp_client::{search, SearchTarget};
use std::{error::Error, time::Duration};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
#[derive(Debug)]
pub struct Device {
url: Url,
client: Client,
}
impl Device {
pub fn new(url: Url) -> Device {
Device {
url,
client: Client::new(),
}
}
pub async fn discover() -> Result<Vec<Device>> {
let search_target = SearchTarget::Custom("roku".into(), "ecp".into());
let mut responses = search(&search_target, Duration::from_secs(3), 2).await?;
let mut devices = vec![];
while let Some(response) = responses.next().await {
let url = Url::parse(response?.location())?;
devices.push(Device {
url,
client: Client::new(),
});
}
Ok(devices)
}
pub async fn apps(&self) -> Result<Apps> {
let url = self.url.join("query/apps")?;
let res = self.client.get(url).send().await?;
let text = res.text().await?;
Ok(from_str(&text)?)
}
pub async fn active_app(&self) -> Result<ActiveApp> {
let url = self.url.join("query/active-app")?;
let res = self.client.get(url).send().await?;
let text = res.text().await?;
Ok(from_str(&text)?)
}
pub async fn media_player(&self) -> Result<MediaPlayer> {
let url = self.url.join("query/media-player")?;
let res = self.client.get(url).send().await?;
let text = res.text().await?;
Ok(from_str(&text)?)
}
pub async fn keydown(&self, key: &Key) -> Result<()> {
let url = self.url.join(&format!("keydown/{}", key.to_string()))?;
self.client.post(url).send().await?;
Ok(())
}
pub async fn keyup(&self, key: &Key) -> Result<()> {
let url = self.url.join(&format!("keyup/{}", key.to_string()))?;
self.client.post(url).send().await?;
Ok(())
}
pub async fn keypress(&self, key: &Key) -> Result<()> {
let url = self.url.join(&format!("keypress/{}", key.to_string()))?;
println!("{}", url);
self.client.post(url).send().await?;
Ok(())
}
pub async fn launch(&self, app: &App) -> Result<()> {
let app_id = match &app.id {
Some(id) => id,
None => {
panic!();
}
};
let url = self.url.join(&format!("launch/{}", app_id))?;
self.client.post(url).send().await?;
Ok(())
}
pub async fn install(&self, app: &App) -> Result<()> {
let app_id = match &app.id {
Some(id) => id,
None => {
panic!();
}
};
let url = self.url.join(&format!("install/{}", app_id))?;
self.client.post(url).send().await?;
Ok(())
}
pub async fn device_info(&self) -> Result<DeviceInfo> {
let url = self.url.join("query/device-info")?;
let res = self.client.get(url).send().await?;
let text = res.text().await?;
println!("{}", text);
Ok(from_str(&text)?)
}
pub async fn input(&self, input: &[(String, String)]) -> Result<()> {
let url = self.url.join("input")?;
self.client.post(url).query(input).send().await?;
Ok(())
}
pub async fn search(&self, search: Search) -> Result<()> {
let search = search.build();
let url = self.url.join("search")?;
self.client.post(url).query(&search).send().await?;
Ok(())
}
}
#[derive(Debug, Deserialize)]
pub struct Apps {
#[serde(rename = "app")]
pub apps: Vec<App>,
}
#[derive(Debug, Deserialize)]
pub struct ActiveApp {
pub app: App,
pub screensaver: Option<Screensaver>,
}
#[derive(Debug, Deserialize)]
pub struct App {
pub id: Option<String>,
#[serde(rename = "$value")]
pub name: String,
pub version: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Screensaver {
pub black: Option<bool>,
pub id: String,
#[serde(rename = "$value")]
pub name: String,
#[serde(rename = "type")]
pub screensaver_type: String,
pub version: String,
}
#[derive(Debug, Deserialize)]
pub struct MediaPlayer {
pub buffering: Option<Buffering>,
pub duration: Option<String>,
pub error: bool,
pub format: Option<Format>,
pub is_live: Option<bool>,
pub new_stream: Option<NewStream>,
pub plugin: Option<Plugin>,
pub position: Option<String>,
pub runtime: Option<String>,
pub state: String,
pub stream_segment: Option<StreamSegment>,
}
#[derive(Debug, Deserialize)]
pub struct Plugin {
pub bandwidth: String,
pub id: String,
pub name: String,
}
#[derive(Debug, Deserialize)]
pub struct Format {
pub audio: String,
pub captions: String,
pub container: String,
pub drm: String,
pub video: String,
pub video_res: String,
}
#[derive(Debug, Deserialize)]
pub struct Buffering {
pub current: u32,
pub max: u32,
pub target: u32,
}
#[derive(Debug, Deserialize)]
pub struct NewStream {
pub speed: String,
}
#[derive(Debug, Deserialize)]
pub struct StreamSegment {
pub bitrate: u32,
pub media_sequence: u32,
pub segment_type: String,
pub time: u32,
}
pub enum Key {
Back,
Backspace,
ChannelDown,
ChannelUp,
Down,
Enter,
FindRemote,
Fwd,
Home,
Info,
InputAV1,
InputHDMI1,
InputHDMI2,
InputHDMI3,
InputHDMI4,
InputTuner,
InstantReplay,
Left,
Play,
PowerOff,
Rev,
Right,
Search,
Select,
Up,
VolumeDown,
VolumeMute,
VolumeUp,
Lit(char),
}
impl Key {
fn to_string(&self) -> String {
match *self {
Key::Back => "Back".to_string(),
Key::Backspace => "Backspace".to_string(),
Key::ChannelDown => "ChannelDown".to_string(),
Key::ChannelUp => "ChannelUp".to_string(),
Key::Down => "Down".to_string(),
Key::Enter => "Enter".to_string(),
Key::FindRemote => "FindRemote".to_string(),
Key::Fwd => "Fwd".to_string(),
Key::Home => "Home".to_string(),
Key::Info => "Info".to_string(),
Key::InputAV1 => "InputAV1".to_string(),
Key::InputHDMI1 => "InputHDMI1".to_string(),
Key::InputHDMI2 => "InputHDMI2".to_string(),
Key::InputHDMI3 => "InputHDMI3".to_string(),
Key::InputHDMI4 => "InputHDMI4".to_string(),
Key::InputTuner => "InputTuner".to_string(),
Key::InstantReplay => "InstantReplay".to_string(),
Key::Left => "Left".to_string(),
Key::Play => "Play".to_string(),
Key::PowerOff => "PowerOff".to_string(),
Key::Rev => "Rev".to_string(),
Key::Right => "Right".to_string(),
Key::Search => "Search".to_string(),
Key::Select => "Select".to_string(),
Key::Up => "Up".to_string(),
Key::VolumeDown => "VolumeDown".to_string(),
Key::VolumeMute => "VolumeMute".to_string(),
Key::VolumeUp => "VolumeUp".to_string(),
Key::Lit(c) => format!("Lit_{}", c),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeviceInfo {
pub advertising_id: String,
pub build_number: String,
pub can_use_wifi_extender: bool,
pub clock_format: String,
pub country: String,
pub davinci_version: String,
pub default_device_name: String,
pub developer_enabled: bool,
pub device_id: String,
pub ethernet_mac: Option<String>,
pub find_remote_is_possible: bool,
pub friendly_device_name: String,
pub friendly_model_name: String,
pub grandcentral_version: String,
pub has_mobile_screensaver: bool,
pub has_play_on_roku: bool,
#[serde(rename = "has-wifi-5G-support")]
pub has_wifi_5g_support: bool,
pub has_wifi_extender: bool,
pub headphones_connected: bool,
pub is_stick: bool,
pub is_tv: bool,
pub keyed_developer_id: String,
pub language: String,
pub locale: String,
pub model_name: String,
pub model_number: String,
pub model_region: String,
pub network_name: String,
pub network_type: String,
pub notifications_enabled: bool,
pub notifications_first_use: bool,
pub power_mode: String,
pub search_channels_enabled: bool,
pub search_enabled: bool,
pub secure_device: bool,
pub serial_number: String,
pub software_build: String,
pub software_version: String,
pub support_url: String,
pub supports_audio_guide: bool,
pub supports_ecs_microphone: bool,
pub supports_ecs_textedit: bool,
pub supports_ethernet: bool,
pub supports_find_remote: bool,
pub supports_private_listening: bool,
pub supports_rva: bool,
pub supports_suspend: bool,
pub supports_wake_on_wlan: bool,
pub time_zone: String,
pub time_zone_auto: bool,
pub time_zone_name: String,
pub time_zone_offset: i32,
pub time_zone_tz: String,
pub udn: String,
pub uptime: u32,
pub user_device_location: String,
pub user_device_name: String,
pub vendor_name: String,
pub voice_search_enabled: bool,
pub wifi_driver: String,
pub wifi_mac: String,
}
pub struct Search {
keyword: String,
launch: Option<bool>,
match_any: Option<bool>,
providers: Option<Vec<String>>,
provider_ids: Option<Vec<String>>,
search_type: Option<SearchType>,
season: Option<u32>,
show_unavailable: Option<bool>,
title: Option<String>,
tmsid: Option<String>,
}
impl Search {
pub fn new(keyword: String) -> Search {
Search {
keyword,
launch: None,
match_any: None,
provider_ids: None,
providers: None,
search_type: None,
season: None,
show_unavailable: None,
title: None,
tmsid: None,
}
}
fn build(self) -> Vec<(String, String)> {
let mut ret = vec![("keyword", self.keyword)];
if let Some(launch) = self.launch {
ret.push(("launch", format!("{}", launch)));
}
if let Some(match_any) = self.match_any {
ret.push(("match-any", format!("{}", match_any)));
}
if let Some(provider_ids) = self.provider_ids {
ret.push(("provider-id", provider_ids.join(",")));
}
if let Some(providers) = self.providers {
ret.push(("provider", providers.join(",")));
}
if let Some(search_type) = self.search_type {
ret.push((
"type",
match search_type {
SearchType::Movie => "movie",
SearchType::TvShow => "tv-show",
SearchType::Person => "person",
SearchType::Channel => "channel",
SearchType::Game => "game",
}
.to_string(),
));
}
if let Some(season) = self.season {
ret.push(("season", format!("{}", season)));
}
if let Some(show_unavailable) = self.show_unavailable {
ret.push(("show-unavailable", format!("{}", show_unavailable)));
}
if let Some(title) = self.title {
ret.push(("title", title));
}
if let Some(tmsid) = self.tmsid {
ret.push(("tmsid", tmsid));
}
ret.into_iter().map(|(k, v)| (k.to_string(), v)).collect()
}
pub fn launch(&mut self, launch: bool) -> &mut Self {
self.launch = Some(launch);
self
}
pub fn match_any(&mut self, match_any: bool) -> &mut Self {
self.match_any = Some(match_any);
self
}
pub fn provider(&mut self, provider: String) -> &mut Self {
match &mut self.providers {
Some(providers) => {
providers.push(provider);
}
None => {
self.providers = Some(vec![]);
}
}
self
}
pub fn provider_id(&mut self, provider_id: String) -> &mut Self {
match &mut self.provider_ids {
Some(provider_ids) => {
provider_ids.push(provider_id);
}
None => {
self.provider_ids = Some(vec![]);
}
}
self
}
pub fn search_type(&mut self, search_type: SearchType) -> &mut Self {
self.search_type = Some(search_type);
self
}
pub fn season(&mut self, season: u32) -> &mut Self {
self.season = Some(season);
self
}
pub fn show_unavailable(&mut self, show_unavailable: bool) -> &mut Self {
self.show_unavailable = Some(show_unavailable);
self
}
pub fn title(&mut self, title: String) -> &mut Self {
self.title = Some(title);
self
}
pub fn tmsid(&mut self, tmsid: String) -> &mut Self {
self.tmsid = Some(tmsid);
self
}
}
pub enum SearchType {
Movie,
TvShow,
Person,
Channel,
Game,
}