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"); } /// Get the text of the given element as a String fn element_to_string(el: &Element) -> String { el.text.to_owned().unwrap_or_default() } impl Speaker { /// Create a new instance of this struct from an IP address pub fn from_ip(ip: IpAddr) -> Result { 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)?), // we slice the UDN to remove "uuid:" uuid: element_to_string(device_description .get_child("UDN") .chain_err(|| ErrorKind::ParseError)?)[5..] .to_string(), }) } /// Get the coordinator for this speaker. pub fn coordinator(&self) -> Result { 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)?; // clean up xml so xmltree can read it let content = content.replace( "", "", ); // parse the topology xml let elements = Element::parse(content.as_bytes()).chain_err(|| ErrorKind::ParseError)?; let zone_players = elements .get_child("ZonePlayers") .chain_err(|| ErrorKind::ParseError)?; // get the group identifier from the given player 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() // 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)?) } /// Call the Sonos SOAP endpoint /// /// # Arguments /// * `endpoint` - The SOAP endpoint to call (eg. MediaRenderer/AVTransport/Control) /// * `service` - The SOAP service to call (eg. urn:schemas-upnp-org:service:AVTransport:1) /// * `action` - The action to call on the soap service (eg. Play) /// * `payload` - XML doc to pass inside the action call body /// * `coordinator` - Whether this SOAP call should be performed on the group coordinator or /// the speaker it was called on pub fn soap( &self, endpoint: &str, service: &str, action: &str, payload: &str, coordinator: bool, ) -> Result { 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#" {payload} "#, 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::() .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()) } } /// Play the current track pub fn play(&self) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "Play", "01", true, )?; Ok(()) } /// Pause the current track pub fn pause(&self) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "Pause", "0", true, )?; Ok(()) } /// Stop the current queue pub fn stop(&self) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "Stop", "0", true, )?; Ok(()) } /// Skip the current track pub fn next(&self) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "Next", "0", true, )?; Ok(()) } /// Go to the previous track pub fn previous(&self) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "Previous", "0", true, )?; Ok(()) } /// Seek to a time on the current track 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!( "0REL_TIME{:02}:{:02}:{:02}", hours, minutes, seconds ), true, )?; Ok(()) } /// Change the track, beginning at 1 pub fn play_queue_item(&self, track: &u64) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "Seek", &format!( "0TRACK_NR{}", track ), true, )?; Ok(()) } /// Remove track at index from queue, beginning at 1 pub fn remove_track(&self, track: &u64) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "RemoveTrackFromQueue", &format!( "0Q:0/{}", track ), true, )?; Ok(()) } /// Add a new track to the end of the queue pub fn queue_track(&self, uri: &str) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "AddURIToQueue", &format!( r#" 0 {} 0 0"#, uri ), true, )?; Ok(()) } /// Add a track to the queue to play next pub fn queue_next(&self, uri: &str) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "AddURIToQueue", &format!( r#" 0 {} 0 1"#, uri ), true, )?; Ok(()) } /// Replace the current track with a new one pub fn play_track(&self, uri: &str) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "SetAVTransportURI", &format!( r#" 0 {} "#, uri ), true, )?; Ok(()) } /// Remove every track from the queue pub fn clear_queue(&self) -> Result<()> { self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "RemoveAllTracksFromQueue", "0", true, )?; Ok(()) } /// Get the current volume pub fn volume(&self) -> Result { let res = self.soap( "MediaRenderer/RenderingControl/Control", "urn:schemas-upnp-org:service:RenderingControl:1", "GetVolume", "0Master", false, )?; let volume = res.get_child("CurrentVolume") .chain_err(|| ErrorKind::ParseError)? .text .to_owned() .chain_err(|| ErrorKind::ParseError)? .parse::() .chain_err(|| ErrorKind::ParseError)?; Ok(volume) } /// Set a new volume from 0-100. 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#" 0 Master {}"#, volume ), false, )?; Ok(()) } /// Check if this player is currently muted pub fn muted(&self) -> Result { let resp = self.soap( "MediaRenderer/RenderingControl/Control", "urn:schemas-upnp-org:service:RenderingControl:1", "GetMute", "0Master", false, )?; Ok(match element_to_string(resp.get_child("CurrentMute") .chain_err(|| ErrorKind::ParseError)?) .as_str() { "1" => true, "0" | _ => false, }) } /// Mute the current player pub fn mute(&self) -> Result<()> { self.soap( "MediaRenderer/RenderingControl/Control", "urn:schemas-upnp-org:service:RenderingControl:1", "SetMute", "0Master1", false, )?; Ok(()) } /// Unmute the current player pub fn unmute(&self) -> Result<()> { self.soap( "MediaRenderer/RenderingControl/Control", "urn:schemas-upnp-org:service:RenderingControl:1", "SetMute", "0Master0", false, )?; Ok(()) } /// Get the transport state of the current player pub fn transport_state(&self) -> Result { let resp = self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "GetTransportInfo", "0", 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, }, ) } /// Get information about the current track pub fn track(&self) -> Result { let resp = self.soap( "MediaRenderer/AVTransport/Control", "urn:schemas-upnp-org:service:AVTransport:1", "GetPositionInfo", "0", 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)?; // convert the given hh:mm:ss to a Duration let duration: Vec = element_to_string(resp.get_child("TrackDuration") .chain_err(|| ErrorKind::ParseError)?) .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)?) .splitn(3, ':') .map(|s| { s.parse::() .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::() .chain_err(|| ErrorKind::ParseError)?, uri: element_to_string(resp.get_child("TrackURI") .chain_err(|| ErrorKind::ParseError)?), duration, running_time, }) } }