Always ask for the current coordinator
Diff
README.md | 2 --
src/device.rs | 196 ++++++++++++++++++++++++++++++++++++++++++--------------------------------------
src/discovery.rs | 8 ++++++--
src/lib.rs | 8 +-------
tests/integration_test.rs | 1 -
5 files changed, 100 insertions(+), 115 deletions(-)
@@ -13,5 +13,3 @@
device.play();
}
```
If your rooms change you have to rediscover.
@@ -1,12 +1,13 @@
extern crate regex;
extern crate reqwest;
extern crate xmltree;
extern crate regex;
use std::net::IpAddr;
use error::*;
use self::xmltree::Element;
use self::reqwest::header::{ContentType, Headers};
use self::regex::Regex;
use std::io::Read;
#[derive(Debug)]
pub struct Speaker {
@@ -17,7 +18,7 @@
pub hardware_version: String,
pub serial_number: String,
pub room: String,
pub coordinator: IpAddr,
pub uuid: String,
}
#[derive(Debug)]
@@ -41,52 +42,13 @@
lazy_static! {
static ref COORDINATOR_REGEX: Regex = Regex::new(r"^https?://(.+?):1400/xml").expect("Failed to create regex");
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()
}
fn get_coordinator(ip: &IpAddr, uuid: &str) -> Result<IpAddr> {
let mut resp = reqwest::get(&format!("http://{}:1400/status/topology", ip))
.chain_err(|| "Failed to grab device description")?;
if !resp.status().is_success() {
return Err("Received a bad response from speaker".into());
}
use std::io::Read;
let mut content = String::new();
resp.read_to_string(&mut content);
let content = content.replace("<?xml-stylesheet type=\"text/xsl\" href=\"/xml/review.xsl\"?>", "");
let elements = Element::parse(content.as_bytes()).chain_err(|| "Failed to parse xml")?;
let zone_players = elements
.get_child("ZonePlayers")
.ok_or("The device gave us a bad response.")?;
let group = zone_players.children.iter()
.find(|ref child| child.attributes.get("uuid").unwrap() == uuid)
.ok_or("Failed to get device group")?
.attributes
.get("group")
.unwrap();
Ok(COORDINATOR_REGEX.captures(zone_players.children.iter()
.find(|ref child| child.attributes.get("coordinator").unwrap_or(&"false".to_string()) == "true" &&
child.attributes.get("group").unwrap_or(&"".to_string()) == group)
.ok_or(format!("Couldn't find coordinator for the given uuid ({})", uuid))?
.attributes
.get("location")
.ok_or("Failed to parse coordinator URL")?)
.ok_or("Failed to parse coordinator URL for IP")?[1]
.parse()
.chain_err(|| "Failed to parse IP address")?)
}
impl Speaker {
@@ -125,12 +87,60 @@
.get_child("roomName")
.ok_or("Failed to parse device description")?),
coordinator: get_coordinator(&ip, &element_to_string(
device_description
.get_child("UDN")
.ok_or("Failed to parse device description")?
)[5..])?
uuid: element_to_string(device_description
.get_child("UDN")
.ok_or("Failed to parse device description")?)[5..]
.to_string(),
})
}
pub fn coordinator(&self) -> Result<IpAddr> {
let mut resp = reqwest::get(&format!("http://{}:1400/status/topology", self.ip))
.chain_err(|| "Failed to grab device description")?;
if !resp.status().is_success() {
return Err("Received a bad response from speaker".into());
}
let mut content = String::new();
resp.read_to_string(&mut content);
let content = content.replace(
"<?xml-stylesheet type=\"text/xsl\" href=\"/xml/review.xsl\"?>",
"",
);
let elements = Element::parse(content.as_bytes()).chain_err(|| "Failed to parse xml")?;
let zone_players = elements
.get_child("ZonePlayers")
.ok_or("The speaker gave us a bad response")?;
let group = zone_players
.children
.iter()
.find(|ref child| child.attributes.get("uuid").unwrap() == &self.uuid)
.ok_or("Failed to get device group")?
.attributes
.get("group")
.unwrap();
Ok(COORDINATOR_REGEX
.captures(zone_players.children.iter()
.find(|ref child|
child.attributes.get("coordinator").unwrap_or(&"false".to_string()) == "true" &&
child.attributes.get("group").unwrap_or(&"".to_string()) == group)
.ok_or(format!("Couldn't find coordinator for the given uuid ({})", self.uuid))?
.attributes
.get("location")
.ok_or("Failed to parse coordinator URL")?)
.ok_or("Failed to parse coordinator URL for IP")?[1]
.parse()
.chain_err(|| "Failed to parse IP address")?)
}
@@ -148,16 +158,19 @@
service: &str,
action: &str,
payload: &str,
coordinator: bool
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/{}", if coordinator { self.coordinator } else { self.ip }, endpoint))
.post(&format!("http://{}:1400/{}", coordinator, endpoint))
.headers(headers)
.body(format!(
r#"
@@ -176,8 +189,8 @@
.send()
.chain_err(|| "Failed to call Sonos controller.")?;
let element = Element::parse(request)
.chain_err(|| "Failed to parse XML from Sonos controller")?;
let element =
Element::parse(request).chain_err(|| "Failed to parse XML from Sonos controller")?;
Ok(
element
@@ -189,7 +202,7 @@
)
}
pub fn play(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
@@ -202,7 +215,7 @@
Ok(())
}
pub fn pause(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
@@ -215,7 +228,7 @@
Ok(())
}
pub fn stop(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
@@ -228,7 +241,7 @@
Ok(())
}
pub fn next(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
@@ -241,7 +254,7 @@
Ok(())
}
pub fn previous(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
@@ -254,7 +267,7 @@
Ok(())
}
pub fn seek(&self, hours: &u8, minutes: &u8, seconds: &u8) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
@@ -272,7 +285,7 @@
Ok(())
}
pub fn play_queue_item(&self, track: &u64) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
@@ -288,7 +301,7 @@
Ok(())
}
pub fn remove_track(&self, track: &u64) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
@@ -304,12 +317,12 @@
Ok(())
}
pub fn queue_track(&self, uri: &str) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"RemoveTrackFromQueue",
"AddURIToQueue",
&format!(
r#"
<InstanceID>0</InstanceID>
@@ -317,27 +330,6 @@
<EnqueuedURIMetaData></EnqueuedURIMetaData>
<DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued>
<EnqueueAsNext>0</EnqueueAsNext>"#,
uri
),
true,
)?;
Ok(())
}
pub fn play_next(&self, uri: &str) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"RemoveTrackFromQueue",
&format!(
r#"
<InstanceID>0</InstanceID>
<EnqueuedURI>{}</EnqueuedURI>
<EnqueuedURIMetaData></EnqueuedURIMetaData>
<DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued>
<EnqueueAsNext>1</EnqueueAsNext>"#,
uri
),
true,
@@ -346,12 +338,12 @@
Ok(())
}
pub fn play_track(&self, uri: &str) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"RemoveTrackFromQueue",
"SetAVTransportURI",
&format!(
r#"
<InstanceID>0</InstanceID>
@@ -365,7 +357,7 @@
Ok(())
}
pub fn clear_queue(&self) -> Result<()> {
self.soap(
"MediaRenderer/AVTransport/Control",
@@ -378,7 +370,7 @@
Ok(())
}
pub fn volume(&self) -> Result<u8> {
let res = self.soap(
"MediaRenderer/RenderingControl/Control",
@@ -399,7 +391,7 @@
Ok(volume)
}
pub fn set_volume(&self, volume: u8) -> Result<()> {
if volume > 100 {
panic!("Volume must be between 0 and 100, got {}.", volume);
@@ -421,7 +413,7 @@
Ok(())
}
pub fn muted(&self) -> Result<bool> {
let resp = self.soap(
"MediaRenderer/RenderingControl/Control",
@@ -431,19 +423,17 @@
false,
)?;
Ok(
match element_to_string(resp.get_child("CurrentMute")
.ok_or("Failed to get current mute status")?)
.as_str()
{
"1" => true,
"0" => false,
_ => false,
},
)
Ok(match element_to_string(resp.get_child("CurrentMute")
.ok_or("Failed to get current mute status")?)
.as_str()
{
"1" => true,
"0" => false,
_ => false,
})
}
pub fn mute(&self) -> Result<()> {
self.soap(
"MediaRenderer/RenderingControl/Control",
@@ -456,7 +446,7 @@
Ok(())
}
pub fn unmute(&self) -> Result<()> {
self.soap(
"MediaRenderer/RenderingControl/Control",
@@ -469,7 +459,7 @@
Ok(())
}
pub fn transport_state(&self) -> Result<TransportState> {
let resp = self.soap(
"MediaRenderer/AVTransport/Control",
@@ -493,7 +483,7 @@
)
}
pub fn track(&self) -> Result<Track> {
let resp = self.soap(
"MediaRenderer/AVTransport/Control",
@@ -1,14 +1,14 @@
extern crate ssdp;
use self::ssdp::FieldMap;
use self::ssdp::header::{HeaderMut, HeaderRef, Man, MX, ST};
use self::ssdp::message::{Multicast, SearchRequest, SearchResponse};
use std::collections::HashMap;
use device::Speaker;
use error::*;
const SONOS_URN: &str = "schemas-upnp-org:device:ZonePlayer:1";
fn get_header(msg: &SearchResponse, header: &str) -> Result<String> {
let bytes = msg.get_raw(header)
.chain_err(|| "Failed to get header from discovery response")?;
@@ -17,6 +17,9 @@
.chain_err(|| "Failed to convert header to UTF-8")
}
pub fn discover() -> Result<Vec<Speaker>> {
let mut request = SearchRequest::new();
@@ -34,7 +37,8 @@
continue;
}
speakers.push(Speaker::from_ip(src.ip()).chain_err(|| "Failed to get device information")?);
speakers.push(Speaker::from_ip(src.ip())
.chain_err(|| "Failed to get device information")?);
}
Ok(speakers)
@@ -19,10 +19,4 @@
pub use device::TransportState;
pub use error::*;
pub fn discover() -> Result<Vec<Speaker>> {
discovery::discover()
}
pub use discovery::discover;
@@ -1,9 +1,8 @@
extern crate sonos;
#[test]
fn can_discover_devices() {
let devices = sonos::discover().unwrap();
println!("{:#?}", devices);
assert!(devices.len() > 0, "No devices discovered");
}