extern crate regex;
extern crate reqwest;
extern crate xmltree;
use std::net::IpAddr;
use std::io::Read;
use std::time::Duration;
use error::*;
pub(crate) use self::xmltree::ParseError;
use self::xmltree::Element;
use self::reqwest::header::{ContentType, Headers};
use self::regex::Regex;
#[derive(Debug)]
pub struct Speaker {
pub ip: IpAddr,
pub model: String,
pub model_number: String,
pub software_version: String,
pub hardware_version: String,
pub serial_number: String,
pub name: String,
pub uuid: String,
}
#[derive(Debug)]
pub struct Track {
pub title: String,
pub artist: String,
pub album: String,
pub queue_position: u64,
pub uri: String,
pub duration: Duration,
pub running_time: Duration,
}
#[derive(Debug, PartialEq)]
pub enum TransportState {
Stopped,
Playing,
PausedPlayback,
PausedRecording,
Recording,
Transitioning,
}
lazy_static! {
static ref COORDINATOR_REGEX: Regex = Regex::new(r"^https?://(.+?):1400/xml")
.expect("Failed to create regex");
}
fn element_to_string(el: &Element) -> String {
el.text.to_owned().unwrap_or_default()
}
impl Speaker {
pub fn from_ip(ip: IpAddr) -> Result<Speaker> {
let resp = reqwest::get(&format!("http://{}:1400/xml/device_description.xml", ip))
.chain_err(|| ErrorKind::DeviceUnreachable)?;
if !resp.status().is_success() {
return Err(ErrorKind::BadResponse.into());
}
let elements = Element::parse(resp)?;
let device_description = elements
.get_child("device")
.chain_err(|| ErrorKind::ParseError)?;
Ok(Speaker {
ip,
model: element_to_string(device_description
.get_child("modelName")
.chain_err(|| ErrorKind::ParseError)?),
model_number: element_to_string(device_description
.get_child("modelNumber")
.chain_err(|| ErrorKind::ParseError)?),
software_version: element_to_string(device_description
.get_child("softwareVersion")
.chain_err(|| ErrorKind::ParseError)?),
hardware_version: element_to_string(device_description
.get_child("hardwareVersion")
.chain_err(|| ErrorKind::ParseError)?),
serial_number: element_to_string(device_description
.get_child("serialNum")
.chain_err(|| ErrorKind::ParseError)?),
name: element_to_string(device_description
.get_child("roomName")
.chain_err(|| ErrorKind::ParseError)?),
uuid: element_to_string(device_description
.get_child("UDN")
.chain_err(|| ErrorKind::ParseError)?)[5..]
.to_string(),
})
}
pub fn coordinator(&self) -> Result<IpAddr> {
let mut resp = reqwest::get(&format!("http://{}:1400/status/topology", self.ip))
.chain_err(|| ErrorKind::DeviceUnreachable)?;
if !resp.status().is_success() {
return Err(ErrorKind::BadResponse.into());
}
let mut content = String::new();
resp.read_to_string(&mut content)
.chain_err(|| ErrorKind::BadResponse)?;
let content = content.replace(
"<?xml-stylesheet type=\"text/xsl\" href=\"/xml/review.xsl\"?>",
"",
);
let elements = Element::parse(content.as_bytes()).chain_err(|| ErrorKind::ParseError)?;
let zone_players = elements
.get_child("ZonePlayers")
.chain_err(|| ErrorKind::ParseError)?;
let group = &zone_players
.children
.iter()
.find(|child| child.attributes["uuid"] == self.uuid)
.chain_err(|| ErrorKind::DeviceNotFound(self.uuid.to_string()))?
.attributes["group"];
Ok(COORDINATOR_REGEX
.captures(zone_players.children.iter()
.find(|child|
child.attributes.get("coordinator").unwrap_or(&"false".to_string()) == "true" &&
child.attributes.get("group").unwrap_or(&"".to_string()) == group)
.chain_err(|| ErrorKind::DeviceNotFound(self.uuid.to_string()))?
.attributes
.get("location")
.chain_err(|| ErrorKind::ParseError)?)
.chain_err(|| ErrorKind::ParseError)?[1]
.parse()
.chain_err(|| ErrorKind::ParseError)?)
}
pub fn soap(
&self,
endpoint: &str,
service: &str,
action: &str,
payload: &str,
coordinator: bool,
) -> Result<Element> {
let mut headers = Headers::new();
headers.set(ContentType::xml());
headers.set_raw("SOAPAction", format!("\"{}#{}\"", service, action));
let client = reqwest::Client::new();
let coordinator = if coordinator {
self.coordinator()?
} else {
self.ip
};
debug!("Running {}#{} on {}", service, action, coordinator);
let request = client
.post(&format!("http://{}:1400/{}", coordinator, endpoint))
.headers(headers)
.body(format!(
r#"
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:{action} xmlns:u="{service}">
{payload}
</u:{action}>
</s:Body>
</s:Envelope>"#,
service = service,
action = action,
payload = payload
))
.send()
.chain_err(|| ErrorKind::DeviceUnreachable)?;
let element = Element::parse(request).chain_err(|| ErrorKind::ParseError)?;
let body = element
.get_child("Body")
.chain_err(|| ErrorKind::ParseError)?;
if let Some(fault) = body.get_child("Fault") {
let error_code = element_to_string(fault
.get_child("detail")
.chain_err(|| ErrorKind::ParseError)?
.get_child("UPnPError")
.chain_err(|| ErrorKind::ParseError)?
.get_child("errorCode")
.chain_err(|| ErrorKind::ParseError)?)
.parse::<u64>()
.chain_err(|| ErrorKind::ParseError)?;
let state = AVTransportError::from(error_code);
error!("Got state {:?} from {}#{} call.", state, service, action);
Err(ErrorKind::from(state).into())
} else {
Ok(body.get_child(format!("{}Response", action))
.chain_err(|| ErrorKind::ParseError)?
.clone())
}
}
pub fn play(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Play",
"<InstanceID>0</InstanceID><Speed>1</Speed>",
true,
)?;
Ok(())
}
pub fn pause(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Pause",
"<InstanceID>0</InstanceID>",
true,
)?;
Ok(())
}
pub fn stop(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Stop",
"<InstanceID>0</InstanceID>",
true,
)?;
Ok(())
}
pub fn next(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Next",
"<InstanceID>0</InstanceID>",
true,
)?;
Ok(())
}
pub fn previous(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Previous",
"<InstanceID>0</InstanceID>",
true,
)?;
Ok(())
}
pub fn seek(&self, time: &Duration) -> Result<()> {
const SECS_PER_MINUTE: u64 = 60;
const MINS_PER_HOUR: u64 = 60;
const SECS_PER_HOUR: u64 = 3600;
let seconds = time.as_secs() % SECS_PER_MINUTE;
let minutes = (time.as_secs() / SECS_PER_MINUTE) % MINS_PER_HOUR;
let hours = time.as_secs() / SECS_PER_HOUR;
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Seek",
&format!(
"<InstanceID>0</InstanceID><Unit>REL_TIME</Unit><Target>{:02}:{:02}:{:02}</Target>",
hours, minutes, seconds
),
true,
)?;
Ok(())
}
pub fn play_queue_item(&self, track: &u64) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Seek",
&format!(
"<InstanceID>0</InstanceID><Unit>TRACK_NR</Unit><Target>{}</Target>",
track
),
true,
)?;
Ok(())
}
pub fn remove_track(&self, track: &u64) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"RemoveTrackFromQueue",
&format!(
"<InstanceID>0</InstanceID><ObjectID>Q:0/{}</ObjectID>",
track
),
true,
)?;
Ok(())
}
pub fn queue_track(&self, uri: &str) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"AddURIToQueue",
&format!(
r#"
<InstanceID>0</InstanceID>
<EnqueuedURI>{}</EnqueuedURI>
<EnqueuedURIMetaData></EnqueuedURIMetaData>
<DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued>
<EnqueueAsNext>0</EnqueueAsNext>"#,
uri
),
true,
)?;
Ok(())
}
pub fn queue_next(&self, uri: &str) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"AddURIToQueue",
&format!(
r#"
<InstanceID>0</InstanceID>
<EnqueuedURI>{}</EnqueuedURI>
<EnqueuedURIMetaData></EnqueuedURIMetaData>
<DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued>
<EnqueueAsNext>1</EnqueueAsNext>"#,
uri
),
true,
)?;
Ok(())
}
pub fn play_track(&self, uri: &str) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"SetAVTransportURI",
&format!(
r#"
<InstanceID>0</InstanceID>
<CurrentURI>{}</CurrentURI>
<CurrentURIMetaData></CurrentURIMetaData>"#,
uri
),
true,
)?;
Ok(())
}
pub fn clear_queue(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"RemoveAllTracksFromQueue",
"<InstanceID>0</InstanceID>",
true,
)?;
Ok(())
}
pub fn volume(&self) -> Result<u8> {
let res = self.soap(
"MediaRenderer/RenderingControl/Control",
"urn:schemas-upnp-org:service:RenderingControl:1",
"GetVolume",
"<InstanceID>0</InstanceID><Channel>Master</Channel>",
false,
)?;
let volume = res.get_child("CurrentVolume")
.chain_err(|| ErrorKind::ParseError)?
.text
.to_owned()
.chain_err(|| ErrorKind::ParseError)?
.parse::<u8>()
.chain_err(|| ErrorKind::ParseError)?;
Ok(volume)
}
pub fn set_volume(&self, volume: u8) -> Result<()> {
if volume > 100 {
panic!("Volume must be between 0 and 100, got {}.", volume);
}
self.soap(
"MediaRenderer/RenderingControl/Control",
"urn:schemas-upnp-org:service:RenderingControl:1",
"SetVolume",
&format!(
r#"
<InstanceID>0</InstanceID>
<Channel>Master</Channel>
<DesiredVolume>{}</DesiredVolume>"#,
volume
),
false,
)?;
Ok(())
}
pub fn muted(&self) -> Result<bool> {
let resp = self.soap(
"MediaRenderer/RenderingControl/Control",
"urn:schemas-upnp-org:service:RenderingControl:1",
"GetMute",
"<InstanceID>0</InstanceID><Channel>Master</Channel>",
false,
)?;
Ok(match element_to_string(resp.get_child("CurrentMute")
.chain_err(|| ErrorKind::ParseError)?)
.as_str()
{
"1" => true,
"0" | _ => false,
})
}
pub fn mute(&self) -> Result<()> {
self.soap(
"MediaRenderer/RenderingControl/Control",
"urn:schemas-upnp-org:service:RenderingControl:1",
"SetMute",
"<InstanceID>0</InstanceID><Channel>Master</Channel><DesiredMute>1</DesiredMute>",
false,
)?;
Ok(())
}
pub fn unmute(&self) -> Result<()> {
self.soap(
"MediaRenderer/RenderingControl/Control",
"urn:schemas-upnp-org:service:RenderingControl:1",
"SetMute",
"<InstanceID>0</InstanceID><Channel>Master</Channel><DesiredMute>0</DesiredMute>",
false,
)?;
Ok(())
}
pub fn transport_state(&self) -> Result<TransportState> {
let resp = self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"GetTransportInfo",
"<InstanceID>0</InstanceID>",
false,
)?;
Ok(
match element_to_string(resp.get_child("CurrentTransportState")
.chain_err(|| ErrorKind::ParseError)?)
.as_str()
{
"PLAYING" => TransportState::Playing,
"PAUSED_PLAYBACK" => TransportState::PausedPlayback,
"PAUSED_RECORDING" => TransportState::PausedRecording,
"RECORDING" => TransportState::Recording,
"TRANSITIONING" => TransportState::Transitioning,
"STOPPED" | _ => TransportState::Stopped,
},
)
}
pub fn track(&self) -> Result<Track> {
let resp = self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"GetPositionInfo",
"<InstanceID>0</InstanceID>",
true,
)?;
let metadata = Element::parse(
element_to_string(resp.get_child("TrackMetaData")
.chain_err(|| ErrorKind::ParseError)?)
.as_bytes(),
).chain_err(|| ErrorKind::ParseError)?;
let metadata = metadata
.get_child("item")
.chain_err(|| ErrorKind::ParseError)?;
let duration: Vec<u64> = element_to_string(resp.get_child("TrackDuration")
.chain_err(|| ErrorKind::ParseError)?)
.splitn(3, ':')
.map(|s| {
s.parse::<u64>()
.chain_err(|| ErrorKind::ParseError)
.unwrap()
})
.collect();
let duration = Duration::from_secs((duration[0] * 3600) + (duration[1] * 60) + duration[2]);
let running_time: Vec<u64> = element_to_string(resp.get_child("RelTime")
.chain_err(|| ErrorKind::ParseError)?)
.splitn(3, ':')
.map(|s| {
s.parse::<u64>()
.chain_err(|| ErrorKind::ParseError)
.unwrap()
})
.collect();
let running_time = Duration::from_secs(
(running_time[0] * 3600) + (running_time[1] * 60) + running_time[2],
);
Ok(Track {
title: element_to_string(metadata
.get_child("title")
.chain_err(|| ErrorKind::ParseError)?),
artist: element_to_string(metadata
.get_child("creator")
.chain_err(|| ErrorKind::ParseError)?),
album: element_to_string(metadata
.get_child("album")
.chain_err(|| ErrorKind::ParseError)?),
queue_position: element_to_string(resp.get_child("Track").chain_err(|| ErrorKind::ParseError)?)
.parse::<u64>()
.chain_err(|| ErrorKind::ParseError)?,
uri: element_to_string(resp.get_child("TrackURI")
.chain_err(|| ErrorKind::ParseError)?),
duration,
running_time,
})
}
}