Files
pmomusic/pmocontrol/src/music_renderer/linkplay_renderer.rs

135 lines
3.8 KiB
Rust

use std::fmt;
use std::time::Duration;
use ureq::Agent;
use crate::DeviceIdentity;
use crate::linkplay_client::{LinkPlayStatus, build_agent, extract_linkplay_host, fetch_status_for_host, percent_encode};
use crate::music_renderer::capabilities::{
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, TransportControl,
VolumeControl,
};
use crate::music_renderer::time_utils::{parse_hhmmss_strict};
use crate::errors::ControlPointError;
use crate::model::{RendererInfo, PlaybackState};
use crate::music_renderer::RendererFromMediaRendererInfo;
use crate::music_renderer::musicrenderer::MusicRendererBackend;
const DEFAULT_HTTP_TIMEOUT_SECS: u64 = 3;
/// Renderer backend for devices exposing the LinkPlay HTTP API.
#[derive(Clone)]
pub struct LinkPlayRenderer {
host: String,
timeout: Duration,
}
impl fmt::Debug for LinkPlayRenderer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LinkPlayRenderer")
.field("host", &self.host)
.finish()
}
}
impl LinkPlayRenderer {
fn agent(&self) -> Agent {
build_agent(self.timeout)
}
fn send_player_command(&self, command: &str) -> Result<(), ControlPointError> {
let url = format!(
"http://{}/httpapi.asp?command=setPlayerCmd:{}",
self.host, command
);
self.agent()
.get(&url)
.call()
.map_err(|_| ControlPointError::ArilycTcpError(format!("LinkPlay command {} failed for {}", command, self.host)))?;
Ok(())
}
fn fetch_status(&self) -> Result<LinkPlayStatus, ControlPointError> {
fetch_status_for_host(&self.host, self.timeout)
}
}
impl RendererFromMediaRendererInfo for LinkPlayRenderer {
fn from_renderer_info(info: &RendererInfo) -> Result<Self, ControlPointError> {
let host = extract_linkplay_host(&info.location())
.ok_or_else(|| ControlPointError::LinkPlayError(format!("Renderer {} has no valid LOCATION host", info.udn())))?;
Ok(Self {
host,
timeout: Duration::from_secs(DEFAULT_HTTP_TIMEOUT_SECS),
})
}
fn to_backend(self) -> MusicRendererBackend {
MusicRendererBackend::LinkPlay(self)
}
}
impl TransportControl for LinkPlayRenderer {
fn play_uri(&self, uri: &str, _meta: &str) -> Result<(), ControlPointError> {
let encoded = percent_encode(uri);
self.send_player_command(&format!("play:{}", encoded))
}
fn play(&self) -> Result<(), ControlPointError> {
self.send_player_command("resume")
}
fn pause(&self) -> Result<(), ControlPointError> {
self.send_player_command("pause")
}
fn stop(&self) -> Result<(), ControlPointError> {
self.send_player_command("stop")
}
fn seek_rel_time(&self, hhmmss: &str) -> Result<(), ControlPointError> {
let secs = parse_hhmmss_strict(hhmmss)?;
self.send_player_command(&format!("seek:{}", secs))
}
}
impl VolumeControl for LinkPlayRenderer {
fn volume(&self) -> Result<u16, ControlPointError> {
Ok(self.fetch_status()?.volume)
}
fn set_volume(&self, v: u16) -> Result<(), ControlPointError> {
let value = v.min(100);
self.send_player_command(&format!("vol:{}", value))
}
fn mute(&self) -> Result<bool, ControlPointError> {
Ok(self.fetch_status()?.mute)
}
fn set_mute(&self, m: bool) -> Result<(), ControlPointError> {
self.send_player_command(if m { "mute:1" } else { "mute:0" })
}
}
impl PlaybackStatus for LinkPlayRenderer {
fn playback_state(&self) -> Result<PlaybackState, ControlPointError> {
Ok(self.fetch_status()?.playback_state())
}
}
impl PlaybackPosition for LinkPlayRenderer {
fn playback_position(&self) -> Result<PlaybackPositionInfo, ControlPointError> {
Ok(self.fetch_status()?.position_info())
}
}