From 7f16481f3ef41c1df80b0d44bbb33a9a50bf7305 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Fri, 21 Feb 2020 13:59:37 +0000 Subject: [PATCH] Move from error-chain to failure crate --- Cargo.toml | 3 ++- src/device.rs | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------ src/discovery.rs | 28 +++++++++++++++------------- src/error.rs | 48 ++++++++++++++++-------------------------------- src/lib.rs | 11 +++-------- 5 files changed, 131 insertions(+), 173 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3259746..1649359 100644 --- a/Cargo.toml +++ a/Cargo.toml @@ -7,12 +7,13 @@ repository = "https://github.com/w4/sonos.rs" keywords = ["sonos", "controller", "music"] readme = "README.md" +edition = "2018" [dependencies] reqwest = { version = "0.10", features = ["blocking"] } log = "0.4" ssdp = "0.7" xmltree = "0.10" -error-chain = "0.12" +failure = "0.1" regex = "1" lazy_static = "1" diff --git a/src/device.rs b/src/device.rs index a81bb29..f14109d 100644 --- a/src/device.rs +++ a/src/device.rs @@ -1,16 +1,13 @@ -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, XMLNode}; -use self::reqwest::header::HeaderMap; -use self::regex::Regex; +use xmltree::{Element, XMLNode}; +use reqwest::header::HeaderMap; +use regex::Regex; +use crate::error::*; +use failure::Error; + #[derive(Debug)] pub struct Speaker { pub ip: IpAddr, @@ -27,7 +24,7 @@ pub struct Track { pub title: String, pub artist: String, - pub album: String, + pub album: Option, pub queue_position: u64, pub uri: String, pub duration: Duration, @@ -56,60 +53,57 @@ impl Speaker { /// Create a new instance of this struct from an IP address - pub fn from_ip(ip: IpAddr) -> Result { - let resp = reqwest::blocking::get(&format!("http://{}:1400/xml/device_description.xml", ip)) - .chain_err(|| ErrorKind::DeviceUnreachable)?; + pub fn from_ip(ip: IpAddr) -> Result { + let resp = reqwest::blocking::get(&format!("http://{}:1400/xml/device_description.xml", ip))?; if !resp.status().is_success() { - return Err(ErrorKind::BadResponse.into()); + return Err(SonosError::BadResponse(resp.status().as_u16()).into()); } let elements = Element::parse(resp)?; let device_description = elements .get_child("device") - .chain_err(|| ErrorKind::ParseError)?; + .ok_or_else(|| SonosError::ParseError("missing root element"))?; Ok(Speaker { ip, model: element_to_string(device_description .get_child("modelName") - .chain_err(|| ErrorKind::ParseError)?), + .ok_or_else(|| SonosError::ParseError("missing model name"))?), model_number: element_to_string(device_description .get_child("modelNumber") - .chain_err(|| ErrorKind::ParseError)?), + .ok_or_else(|| SonosError::ParseError("missing model number"))?), software_version: element_to_string(device_description .get_child("softwareVersion") - .chain_err(|| ErrorKind::ParseError)?), + .ok_or_else(|| SonosError::ParseError("missing software version"))?), hardware_version: element_to_string(device_description .get_child("hardwareVersion") - .chain_err(|| ErrorKind::ParseError)?), + .ok_or_else(|| SonosError::ParseError("missing hardware version"))?), serial_number: element_to_string(device_description .get_child("serialNum") - .chain_err(|| ErrorKind::ParseError)?), + .ok_or_else(|| SonosError::ParseError("missing serial number"))?), name: element_to_string(device_description .get_child("roomName") - .chain_err(|| ErrorKind::ParseError)?), + .ok_or_else(|| SonosError::ParseError("missing room name"))?), // we slice the UDN to remove "uuid:" uuid: element_to_string(device_description .get_child("UDN") - .chain_err(|| ErrorKind::ParseError)?)[5..] + .ok_or_else(|| SonosError::ParseError("missing UDN"))?)[5..] .to_string(), }) } /// Get the coordinator for this speaker. #[deprecated(note = "Broken on Sonos 9.1")] - pub fn coordinator(&self) -> Result { - let mut resp = reqwest::blocking::get(&format!("http://{}:1400/status/topology", self.ip)) - .chain_err(|| ErrorKind::DeviceUnreachable)?; + pub fn coordinator(&self) -> Result { + let mut resp = reqwest::blocking::get(&format!("http://{}:1400/status/topology", self.ip))?; if !resp.status().is_success() { - return Err(ErrorKind::BadResponse.into()); + return Err(SonosError::BadResponse(resp.status().as_u16()).into()); } let mut content = String::new(); - resp.read_to_string(&mut content) - .chain_err(|| ErrorKind::BadResponse)?; + resp.read_to_string(&mut content)?; // clean up xml so xmltree can read it let content = content.replace( @@ -118,7 +112,7 @@ ); // parse the topology xml - let elements = Element::parse(content.as_bytes()).chain_err(|| ErrorKind::ParseError)?; + let elements = Element::parse(content.as_bytes())?; if elements.children.is_empty() { // on Sonos 9.1 this API will always return an empty string in which case we'll return @@ -128,7 +122,7 @@ let zone_players = elements .get_child("ZonePlayers") - .chain_err(|| ErrorKind::ParseError)?; + .ok_or_else(|| SonosError::ParseError("missing root element"))?; // get the group identifier from the given player let group = &zone_players @@ -138,25 +132,26 @@ .filter(Option::is_some) .map(Option::unwrap) .find(|child| child.attributes["uuid"] == self.uuid) - .chain_err(|| ErrorKind::DeviceNotFound(self.uuid.to_string()))? + .ok_or_else(|| SonosError::DeviceNotFound(self.uuid.to_string()))? .attributes["group"]; + + let parent = zone_players.children.iter() + // get the coordinator for the given group + .map(XMLNode::as_element) + .filter(Option::is_some) + .map(Option::unwrap) + .find(|child| + child.attributes.get("coordinator").unwrap_or(&"false".to_string()) == "true" && + child.attributes.get("group").unwrap_or(&"".to_string()) == group) + .ok_or_else(|| SonosError::DeviceNotFound(self.uuid.to_string()))? + .attributes + .get("location") + .ok_or_else(|| SonosError::ParseError("missing group identifier"))?; Ok(COORDINATOR_REGEX - .captures(zone_players.children.iter() - .map(XMLNode::as_element) - .filter(Option::is_some) - .map(Option::unwrap) - // get the coordinator for the given group - .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)?) + .captures(parent) + .ok_or_else(|| SonosError::ParseError("couldn't parse coordinator url"))?[1] + .parse()?) } /// Call the Sonos SOAP endpoint @@ -175,11 +170,10 @@ action: &str, payload: &str, coordinator: bool, - ) -> Result { + ) -> Result { let mut headers = HeaderMap::new(); - headers.insert("Content-Type", "application/xml".parse().unwrap()); - headers.insert("SOAPAction", format!("\"{}#{}\"", service, action).parse() - .map_err(|_| "service/action caused an invalid header")?); + headers.insert("Content-Type", "application/xml".parse()?); + headers.insert("SOAPAction", format!("\"{}#{}\"", service, action).parse()?); let client = reqwest::blocking::Client::new(); let coordinator = if coordinator { @@ -207,38 +201,36 @@ action = action, payload = payload )) - .send() - .chain_err(|| ErrorKind::DeviceUnreachable)?; + .send()?; - let element = Element::parse(request).chain_err(|| ErrorKind::ParseError)?; + let element = Element::parse(request)?; let body = element .get_child("Body") - .chain_err(|| ErrorKind::ParseError)?; + .ok_or_else(|| SonosError::ParseError("missing root element"))?; 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::() - .chain_err(|| ErrorKind::ParseError)?; + .map(|c| c.get_child("UPnPError")) + .flatten() + .map(|c| c.get_child("errorCode")) + .flatten() + .ok_or_else(|| SonosError::ParseError("failed to parse error"))?) + .parse::()?; let state = AVTransportError::from(error_code); error!("Got state {:?} from {}#{} call.", state, service, action); - Err(ErrorKind::from(state).into()) + Err(SonosError::from(state).into()) } else { Ok(body.get_child(format!("{}Response", action)) - .chain_err(|| ErrorKind::ParseError)? + .ok_or_else(|| SonosError::ParseError("failed to find root element"))? .clone()) } } /// Play the current track - pub fn play(&self) -> Result<()> { + pub fn play(&self) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -251,7 +243,7 @@ } /// Pause the current track - pub fn pause(&self) -> Result<()> { + pub fn pause(&self) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -264,7 +256,7 @@ } /// Stop the current queue - pub fn stop(&self) -> Result<()> { + pub fn stop(&self) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -277,7 +269,7 @@ } /// Skip the current track - pub fn next(&self) -> Result<()> { + pub fn next(&self) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -290,7 +282,7 @@ } /// Go to the previous track - pub fn previous(&self) -> Result<()> { + pub fn previous(&self) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -303,7 +295,7 @@ } /// Seek to a time on the current track - pub fn seek(&self, time: &Duration) -> Result<()> { + pub 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; @@ -327,7 +319,7 @@ } /// Change the track, beginning at 1 - pub fn play_queue_item(&self, track: &u64) -> Result<()> { + pub fn play_queue_item(&self, track: &u64) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -343,7 +335,7 @@ } /// Remove track at index from queue, beginning at 1 - pub fn remove_track(&self, track: &u64) -> Result<()> { + pub fn remove_track(&self, track: &u64) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -359,7 +351,7 @@ } /// Add a new track to the end of the queue - pub fn queue_track(&self, uri: &str) -> Result<()> { + pub fn queue_track(&self, uri: &str) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -380,7 +372,7 @@ } /// Add a track to the queue to play next - pub fn queue_next(&self, uri: &str) -> Result<()> { + pub fn queue_next(&self, uri: &str) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -401,7 +393,7 @@ } /// Replace the current track with a new one - pub fn play_track(&self, uri: &str) -> Result<()> { + pub fn play_track(&self, uri: &str) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -420,7 +412,7 @@ } /// Remove every track from the queue - pub fn clear_queue(&self) -> Result<()> { + pub fn clear_queue(&self) -> Result<(), Error> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -433,7 +425,7 @@ } /// Get the current volume - pub fn volume(&self) -> Result { + pub fn volume(&self) -> Result { let res = self.soap( "MediaRenderer/RenderingControl/Control", "urn:schemas-upnp-org:service:RenderingControl:1", @@ -442,15 +434,14 @@ false, )?; - let volume = element_to_string(res.get_child("CurrentVolume").chain_err(|| ErrorKind::ParseError)?) - .parse::() - .chain_err(|| ErrorKind::ParseError)?; + let volume = element_to_string(res.get_child("CurrentVolume").ok_or_else(|| SonosError::ParseError("failed to find CurrentVolume element"))?) + .parse::()?; Ok(volume) } /// Set a new volume from 0-100. - pub fn set_volume(&self, volume: u8) -> Result<()> { + pub fn set_volume(&self, volume: u8) -> Result<(), Error> { if volume > 100 { panic!("Volume must be between 0 and 100, got {}.", volume); } @@ -472,7 +463,7 @@ } /// Check if this player is currently muted - pub fn muted(&self) -> Result { + pub fn muted(&self) -> Result { let resp = self.soap( "MediaRenderer/RenderingControl/Control", "urn:schemas-upnp-org:service:RenderingControl:1", @@ -482,7 +473,7 @@ )?; Ok(match element_to_string(resp.get_child("CurrentMute") - .chain_err(|| ErrorKind::ParseError)?) + .ok_or_else(|| SonosError::ParseError("failed to find CurrentMute element"))?) .as_str() { "1" => true, @@ -491,7 +482,7 @@ } /// Mute the current player - pub fn mute(&self) -> Result<()> { + pub fn mute(&self) -> Result<(), Error> { self.soap( "MediaRenderer/RenderingControl/Control", "urn:schemas-upnp-org:service:RenderingControl:1", @@ -504,7 +495,7 @@ } /// Unmute the current player - pub fn unmute(&self) -> Result<()> { + pub fn unmute(&self) -> Result<(), Error> { self.soap( "MediaRenderer/RenderingControl/Control", "urn:schemas-upnp-org:service:RenderingControl:1", @@ -517,7 +508,7 @@ } /// Get the transport state of the current player - pub fn transport_state(&self) -> Result { + pub fn transport_state(&self) -> Result { let resp = self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -528,7 +519,7 @@ Ok( match element_to_string(resp.get_child("CurrentTransportState") - .chain_err(|| ErrorKind::ParseError)?) + .ok_or_else(|| SonosError::ParseError("failed to find CurrentTransportState element"))?) .as_str() { "PLAYING" => TransportState::Playing, @@ -542,7 +533,7 @@ } /// Get information about the current track - pub fn track(&self) -> Result { + pub fn track(&self) -> Result { let resp = self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", @@ -553,52 +544,43 @@ let metadata = Element::parse( element_to_string(resp.get_child("TrackMetaData") - .chain_err(|| ErrorKind::ParseError)?) + .ok_or_else(|| SonosError::ParseError("failed to find TrackMetaData element"))?) .as_bytes(), - ).chain_err(|| ErrorKind::ParseError)?; + )?; let metadata = metadata .get_child("item") - .chain_err(|| ErrorKind::ParseError)?; + .ok_or_else(|| SonosError::ParseError("failed to find item element"))?; // convert the given hh:mm:ss to a Duration - let duration: Vec = element_to_string(resp.get_child("TrackDuration") - .chain_err(|| ErrorKind::ParseError)?) + let mut duration = element_to_string(resp.get_child("TrackDuration") + .ok_or_else(|| SonosError::ParseError("failed to find TrackDuration element"))?) .splitn(3, ':') - .map(|s| { - s.parse::() - .chain_err(|| ErrorKind::ParseError) - .unwrap() - }) - .collect(); - let duration = Duration::from_secs((duration[0] * 3600) + (duration[1] * 60) + duration[2]); - - let running_time: Vec = element_to_string(resp.get_child("RelTime") - .chain_err(|| ErrorKind::ParseError)?) + .map(|s| s.parse::()) + .collect::>>(); + let duration = Duration::from_secs((duration.remove(0)? * 3600) + (duration.remove(0)? * 60) + duration.remove(0)?); + + let mut running_time = element_to_string(resp.get_child("RelTime") + .ok_or_else(|| SonosError::ParseError("failed to find RelTime element"))?) .splitn(3, ':') - .map(|s| { - s.parse::() - .chain_err(|| ErrorKind::ParseError) - .unwrap() - }) - .collect(); + .map(|s| s.parse::()) + .collect::>>(); let running_time = Duration::from_secs( - (running_time[0] * 3600) + (running_time[1] * 60) + running_time[2], + (running_time.remove(0)? * 3600) + (running_time.remove(0)? * 60) + running_time.remove(0)?, ); Ok(Track { title: element_to_string(metadata .get_child("title") - .chain_err(|| ErrorKind::ParseError)?), + .ok_or_else(|| SonosError::ParseError("failed to find title element"))?), artist: element_to_string(metadata .get_child("creator") - .chain_err(|| ErrorKind::ParseError)?), - album: metadata.get_child("album").map(element_to_string).unwrap_or_else(|| String::from("Unknown")), - queue_position: element_to_string(resp.get_child("Track").chain_err(|| ErrorKind::ParseError)?) - .parse::() - .chain_err(|| ErrorKind::ParseError)?, + .ok_or_else(|| SonosError::ParseError("failed to find creator element"))?), + album: metadata.get_child("album").map(element_to_string), + queue_position: element_to_string(resp.get_child("Track").ok_or_else(|| SonosError::ParseError("failed to find track element"))?) + .parse::()?, uri: element_to_string(resp.get_child("TrackURI") - .chain_err(|| ErrorKind::ParseError)?), + .ok_or_else(|| SonosError::ParseError("failed to find TrackURI element"))?), duration, running_time, }) diff --git a/src/discovery.rs b/src/discovery.rs index 99468d6..9eaa530 100644 --- a/src/discovery.rs +++ a/src/discovery.rs @@ -1,25 +1,25 @@ -extern crate ssdp; - -use self::ssdp::FieldMap; -use self::ssdp::header::{HeaderMut, HeaderRef, Man, MX, ST}; -use self::ssdp::message::{Multicast, SearchRequest, SearchResponse}; -pub(crate) use self::ssdp::SSDPError; -use device::Speaker; -use error::*; +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"; /// Convenience method to grab a header from an SSDP search as a string. -fn get_header(msg: &SearchResponse, header: &str) -> Result { - let bytes = msg.get_raw(header).chain_err(|| ErrorKind::ParseError)?; +fn get_header(msg: &SearchResponse, header: &str) -> Result { + let bytes = msg.get_raw(header).ok_or_else(|| SonosError::ParseError("failed to find header"))?; - String::from_utf8(bytes[0].clone()).chain_err(|| ErrorKind::ParseError) + Ok(String::from_utf8(bytes[0].clone())?) } /// Discover all speakers on the current network. /// /// This method **will** block for 2 seconds while waiting for broadcast responses. -pub fn discover() -> Result> { +pub fn discover() -> Result, Error> { let mut request = SearchRequest::new(); request.set(Man); // required header for discovery @@ -28,7 +28,7 @@ let mut speakers = Vec::new(); - for (msg, src) in request.multicast()? { + for (msg, src) in request.multicast().map_err(SyncFailure::new)? { let usn = get_header(&msg, "USN")?; if !usn.contains(SONOS_URN) { @@ -36,7 +36,7 @@ continue; } - speakers.push(Speaker::from_ip(src.ip()).chain_err(|| ErrorKind::ParseError)?); + speakers.push(Speaker::from_ip(src.ip())?); } Ok(speakers) diff --git a/src/error.rs b/src/error.rs index f0b2719..3eb7238 100644 --- a/src/error.rs +++ a/src/error.rs @@ -1,40 +1,20 @@ -error_chain! { - errors { - AVTransportError(error: AVTransportError) { - description("An error occurred from AVTransport") - display("Received error {:?} from Sonos speaker", error) - } - - ParseError { - description("An error occurred when attempting to parse SOAP XML from Sonos") - display("Failed to parse Sonos response XML") - } - - DeviceUnreachable { - description("An error occurred when attempting to contact the device") - display("Failed to call Sonos endpoint") - } - - BadResponse { - description("The device returned a bad response") - display("Received a non-success response from Sonos") - } - - DeviceNotFound(identifier: String) { - description("An error occurred when trying to find device") - display("Couldn't find a device by the given identifier ({})", identifier) - } - } - - foreign_links { - Discovery(::discovery::SSDPError) #[doc = "An error occurred while attempting to discover devices"]; - XMLParse(::device::ParseError) #[doc = "An error occurred while parsing device response"]; - } +#[derive(Debug, Fail)] +pub enum SonosError { + #[fail(display = "Received error {:?} from Sonos speaker", 0)] + AVTransportError(AVTransportError), + #[fail(display = "Failed to parse Sonos response XML ({})", 0)] + ParseError(&'static str), + #[fail(display = "Failed to call Sonos endpoint")] + DeviceUnreachable, + #[fail(display = "Received a non-success ({}) response from Sonos", 0)] + BadResponse(u16), + #[fail(display = "Couldn't find a device by the given identifier ({})", 0)] + DeviceNotFound(String), } -impl From for ErrorKind { +impl From for SonosError { fn from(error: AVTransportError) -> Self { - ErrorKind::AVTransportError(error) + SonosError::AVTransportError(error) } } diff --git a/src/lib.rs b/src/lib.rs index 9f34485..b98b845 100644 --- a/src/lib.rs +++ a/src/lib.rs @@ -1,11 +1,6 @@ -#[macro_use] -extern crate log; - -#[macro_use] -extern crate error_chain; - -#[macro_use] -extern crate lazy_static; +#[macro_use] extern crate log; +#[macro_use] extern crate failure; +#[macro_use] extern crate lazy_static; mod discovery; mod device; -- rgit 0.1.3