Move to async-based API
Diff
Cargo.toml | 5 +++--
src/device.rs | 148 +++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
src/discovery.rs | 43 +++++++++++++++++++++----------------------
3 files changed, 91 insertions(+), 105 deletions(-)
@@ -10,9 +10,10 @@
edition = "2018"
[dependencies]
reqwest = { version = "0.10", features = ["blocking"] }
reqwest = "0.10"
log = "0.4"
ssdp = "0.7"
ssdp-client = "0.5"
futures = "0.3"
xmltree = "0.10"
failure = "0.1"
regex = "1"
@@ -1,6 +1,6 @@
use std::net::IpAddr;
use std::io::Read;
use std::time::Duration;
use xmltree::{Element, XMLNode};
use reqwest::header::HeaderMap;
use regex::Regex;
@@ -53,14 +53,14 @@
impl Speaker {
pub fn from_ip(ip: IpAddr) -> Result<Speaker, Error> {
let resp = reqwest::blocking::get(&format!("http://{}:1400/xml/device_description.xml", ip))?;
pub async fn from_ip(ip: IpAddr) -> Result<Speaker, Error> {
let resp = reqwest::get(&format!("http://{}:1400/xml/device_description.xml", ip)).await?;
if !resp.status().is_success() {
return Err(SonosError::BadResponse(resp.status().as_u16()).into());
}
let elements = Element::parse(resp)?;
let elements = Element::parse(resp.bytes().await?.as_ref())?;
let device_description = elements
.get_child("device")
.ok_or_else(|| SonosError::ParseError("missing root element"))?;
@@ -95,22 +95,15 @@
#[deprecated(note = "Broken on Sonos 9.1")]
pub fn coordinator(&self) -> Result<IpAddr, Error> {
let mut resp = reqwest::blocking::get(&format!("http://{}:1400/status/topology", self.ip))?;
pub async fn coordinator(&self) -> Result<IpAddr, Error> {
let resp = reqwest::get(&format!("http://{}:1400/status/topology", self.ip)).await?;
if !resp.status().is_success() {
return Err(SonosError::BadResponse(resp.status().as_u16()).into());
}
let mut content = String::new();
resp.read_to_string(&mut content)?;
let content = resp.text().await?;
let content = content.replace(
"<?xml-stylesheet type=\"text/xsl\" href=\"/xml/review.xsl\"?>",
"",
);
let elements = Element::parse(content.as_bytes())?;
@@ -163,7 +156,7 @@
pub fn soap(
pub async fn soap(
&self,
endpoint: &str,
service: &str,
@@ -175,9 +168,9 @@
headers.insert("Content-Type", "application/xml".parse()?);
headers.insert("SOAPAction", format!("\"{}#{}\"", service, action).parse()?);
let client = reqwest::blocking::Client::new();
let client = reqwest::Client::new();
let coordinator = if coordinator {
self.coordinator()?
self.coordinator().await?
} else {
self.ip
};
@@ -201,9 +194,10 @@
action = action,
payload = payload
))
.send()?;
.send()
.await?;
let element = Element::parse(request)?;
let element = Element::parse(request.bytes().await?.as_ref())?;
let body = element
.get_child("Body")
@@ -230,72 +224,72 @@
}
pub fn play(&self) -> Result<(), Error> {
pub async fn play(&self) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Play",
"<InstanceID>0</InstanceID><Speed>1</Speed>",
true,
)?;
).await?;
Ok(())
}
pub fn pause(&self) -> Result<(), Error> {
pub async fn pause(&self) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Pause",
"<InstanceID>0</InstanceID>",
true,
)?;
).await?;
Ok(())
}
pub fn stop(&self) -> Result<(), Error> {
pub async fn stop(&self) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Stop",
"<InstanceID>0</InstanceID>",
true,
)?;
).await?;
Ok(())
}
pub fn next(&self) -> Result<(), Error> {
pub async fn next(&self) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Next",
"<InstanceID>0</InstanceID>",
true,
)?;
).await?;
Ok(())
}
pub fn previous(&self) -> Result<(), Error> {
pub async fn previous(&self) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"Previous",
"<InstanceID>0</InstanceID>",
true,
)?;
).await?;
Ok(())
}
pub fn seek(&self, time: &Duration) -> Result<(), Error> {
pub async fn seek(&self, time: &Duration) -> Result<(), Error> {
const SECS_PER_MINUTE: u64 = 60;
const MINS_PER_HOUR: u64 = 60;
const SECS_PER_HOUR: u64 = 3600;
@@ -313,13 +307,13 @@
hours, minutes, seconds
),
true,
)?;
).await?;
Ok(())
}
pub fn play_queue_item(&self, track: &u64) -> Result<(), Error> {
pub async fn play_queue_item(&self, track: &u64) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
@@ -329,13 +323,13 @@
track
),
true,
)?;
).await?;
Ok(())
}
pub fn remove_track(&self, track: &u64) -> Result<(), Error> {
pub async fn remove_track(&self, track: &u64) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
@@ -345,13 +339,13 @@
track
),
true,
)?;
).await?;
Ok(())
}
pub fn queue_track(&self, uri: &str) -> Result<(), Error> {
pub async fn queue_track(&self, uri: &str) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
@@ -366,13 +360,13 @@
uri
),
true,
)?;
).await?;
Ok(())
}
pub fn queue_next(&self, uri: &str) -> Result<(), Error> {
pub async fn queue_next(&self, uri: &str) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
@@ -387,13 +381,13 @@
uri
),
true,
)?;
).await?;
Ok(())
}
pub fn play_track(&self, uri: &str) -> Result<(), Error> {
pub async fn play_track(&self, uri: &str) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
@@ -406,42 +400,42 @@
uri
),
true,
)?;
).await?;
Ok(())
}
pub fn clear_queue(&self) -> Result<(), Error> {
pub async fn clear_queue(&self) -> Result<(), Error> {
self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"RemoveAllTracksFromQueue",
"<InstanceID>0</InstanceID>",
true,
)?;
).await?;
Ok(())
}
pub fn volume(&self) -> Result<u8, Error> {
pub async fn volume(&self) -> Result<u8, Error> {
let res = self.soap(
"MediaRenderer/RenderingControl/Control",
"urn:schemas-upnp-org:service:RenderingControl:1",
"GetVolume",
"<InstanceID>0</InstanceID><Channel>Master</Channel>",
false,
)?;
).await?;
let volume = element_to_string(res.get_child("CurrentVolume").ok_or_else(|| SonosError::ParseError("failed to find CurrentVolume element"))?)
.parse::<u8>()?;
let volume_element = res.get_child("CurrentVolume").ok_or_else(|| SonosError::ParseError("failed to find CurrentVolume element"))?;
let volume = element_to_string(volume_element).parse::<u8>()?;
Ok(volume)
}
pub fn set_volume(&self, volume: u8) -> Result<(), Error> {
pub async fn set_volume(&self, volume: u8) -> Result<(), Error> {
if volume > 100 {
panic!("Volume must be between 0 and 100, got {}.", volume);
}
@@ -458,89 +452,87 @@
volume
),
false,
)?;
).await?;
Ok(())
}
pub fn muted(&self) -> Result<bool, Error> {
pub async fn muted(&self) -> Result<bool, Error> {
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")
.ok_or_else(|| SonosError::ParseError("failed to find CurrentMute element"))?)
.as_str()
{
).await?;
let mute_element = resp.get_child("CurrentMute").ok_or_else(|| SonosError::ParseError("failed to find CurrentMute element"))?;
Ok(match element_to_string(mute_element).as_str() {
"1" => true,
"0" | _ => false,
})
}
pub fn mute(&self) -> Result<(), Error> {
pub async fn mute(&self) -> Result<(), Error> {
self.soap(
"MediaRenderer/RenderingControl/Control",
"urn:schemas-upnp-org:service:RenderingControl:1",
"SetMute",
"<InstanceID>0</InstanceID><Channel>Master</Channel><DesiredMute>1</DesiredMute>",
false,
)?;
).await?;
Ok(())
}
pub fn unmute(&self) -> Result<(), Error> {
pub async fn unmute(&self) -> Result<(), Error> {
self.soap(
"MediaRenderer/RenderingControl/Control",
"urn:schemas-upnp-org:service:RenderingControl:1",
"SetMute",
"<InstanceID>0</InstanceID><Channel>Master</Channel><DesiredMute>0</DesiredMute>",
false,
)?;
).await?;
Ok(())
}
pub fn transport_state(&self) -> Result<TransportState, Error> {
pub async fn transport_state(&self) -> Result<TransportState, Error> {
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")
.ok_or_else(|| SonosError::ParseError("failed to find CurrentTransportState element"))?)
.as_str()
{
"PLAYING" => TransportState::Playing,
"PAUSED_PLAYBACK" => TransportState::PausedPlayback,
"PAUSED_RECORDING" => TransportState::PausedRecording,
"RECORDING" => TransportState::Recording,
"TRANSITIONING" => TransportState::Transitioning,
"STOPPED" | _ => TransportState::Stopped,
},
)
).await?;
let transport_state_element = resp.get_child("CurrentTransportState")
.ok_or_else(|| SonosError::ParseError("failed to find CurrentTransportState element"))?;
Ok(match element_to_string(transport_state_element).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, Error> {
pub async fn track(&self) -> Result<Track, Error> {
let resp = self.soap(
"MediaRenderer/AVTransport/Control",
"urn:schemas-upnp-org:service:AVTransport:1",
"GetPositionInfo",
"<InstanceID>0</InstanceID>",
true,
)?;
).await?;
let metadata = element_to_string(resp.get_child("TrackMetaData")
.ok_or_else(|| SonosError::ParseError("failed to find TrackMetaData element"))?);
@@ -1,42 +1,35 @@
use ssdp::FieldMap;
use ssdp::header::{HeaderMut, HeaderRef, Man, MX, ST};
use ssdp::message::{Multicast, SearchRequest, SearchResponse};
use failure::{Error, SyncFailure};
use crate::device::Speaker;
use crate::error::*;
const SONOS_URN: &str = "schemas-upnp-org:device:ZonePlayer:1";
use std::time::Duration;
use regex::Regex;
fn get_header(msg: &SearchResponse, header: &str) -> Result<String, Error> {
let bytes = msg.get_raw(header).ok_or_else(|| SonosError::ParseError("failed to find header"))?;
use ssdp_client::URN;
use failure::Error;
Ok(String::from_utf8(bytes[0].clone())?)
use futures::prelude::*;
lazy_static! {
static ref LOCATION_REGEX: Regex = Regex::new(r"^https?://(.+?):1400/xml")
.expect("Failed to create regex");
}
pub fn discover() -> Result<Vec<Speaker>, Error> {
let mut request = SearchRequest::new();
pub async fn discover() -> Result<Vec<Speaker>, Error> {
let search_target = URN::device("schemas-upnp-org", "ZonePlayer", 1).into();
let timeout = Duration::from_secs(2);
let responses = ssdp_client::search(&search_target, timeout, 1).await?;
futures::pin_mut!(responses);
request.set(Man);
request.set(MX(2));
request.set(ST::Target(FieldMap::URN(String::from(SONOS_URN))));
let mut speakers = Vec::new();
for (msg, src) in request.multicast().map_err(SyncFailure::new)? {
let usn = get_header(&msg, "USN")?;
while let Some(response) = responses.next().await {
let response = response?;
if !usn.contains(SONOS_URN) {
error!("Misbehaving client responded to our discovery ({})", usn);
continue;
if let Some(ip) = LOCATION_REGEX.captures(response.location()).and_then(|x| x.get(1)).map(|x| x.as_str()) {
speakers.push(Speaker::from_ip(ip.parse()?).await?);
}
speakers.push(Speaker::from_ip(src.ip())?);
}
Ok(speakers)