From 9df5e979ab704421da39a2c75283d645892b239e Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 09 Dec 2017 22:32:50 +0000 Subject: [PATCH] Better SOAP error logging --- src/device.rs | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- src/error.rs | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/integration_test.rs | 7 +++++++ 3 files changed, 164 insertions(+), 9 deletions(-) diff --git a/src/device.rs b/src/device.rs index ef7b8ce..7ef7ff8 100644 --- a/src/device.rs +++ a/src/device.rs @@ -38,6 +38,9 @@ Stopped, Playing, PausedPlayback, + PausedRecording, + Recording, + NoMediaPresent, Transitioning, } @@ -164,7 +167,7 @@ ) -> Result { let mut headers = Headers::new(); headers.set(ContentType::xml()); - headers.set_raw("SOAPAction", format!("{}#{}", service, action)); + headers.set_raw("SOAPAction", format!("\"{}#{}\"", service, action)); let client = reqwest::Client::new(); let coordinator = if coordinator { self.coordinator()? } else { self.ip }; @@ -194,14 +197,28 @@ let element = Element::parse(request).chain_err(|| "Failed to parse XML from Sonos controller")?; - Ok( - element - .get_child("Body") - .ok_or("Failed to get body element")? + let body = element.get_child("Body") + .ok_or("Failed to get body 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)?; + + 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)) - .ok_or("Failed to find response element")? - .clone(), - ) + .ok_or(format!("Failed to find response element {:?}", element))? + .clone()) + } } /// Play the current track @@ -340,6 +357,26 @@ 0 0"#, + 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#" + 0 + {} + + 0 + 1"#, uri ), true, @@ -487,6 +524,9 @@ "STOPPED" => TransportState::Stopped, "PLAYING" => TransportState::Playing, "PAUSED_PLAYBACK" => TransportState::PausedPlayback, + "PAUSED_RECORDING" => TransportState::PausedRecording, + "RECORDING" => TransportState::Recording, + "NO_MEDIA_PRESENT" => TransportState::NoMediaPresent, "TRANSITIONING" => TransportState::Transitioning, _ => TransportState::Stopped, }, diff --git a/src/error.rs b/src/error.rs index 0c6d43c..5c0b0fa 100644 --- a/src/error.rs +++ a/src/error.rs @@ -1,1 +1,109 @@ -error_chain!{} +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") + } + } +} + +impl From for ErrorKind { + fn from(error: AVTransportError) -> Self { + ErrorKind::AVTransportError(error) + } +} + +#[derive(Debug)] +pub enum AVTransportError { + /// No action by that name at this service. + InvalidAction = 401, + /// Could be any of the following: not enough in args, too many in args, no in arg by that name, + /// one or more in args are of the wrong data type. + InvalidArgs = 402, + /// No state variable by that name at this service. + InvalidVar = 404, + /// May be returned in current state of service prevents invoking that action. + ActionFailed = 501, + /// The immediate transition from current transport state to desired transport state is not + /// supported by this device. + TransitionNotAvailable = 701, + /// The media does not contain any contents that can be played. + NoContents = 702, + /// The media cannot be read (e.g., because of dust or a scratch). + ReadError = 703, + /// The storage format of the currently loaded media is not supported + FormatNotSupported = 704, + /// The transport is “hold locked”. + TransportLocked = 705, + /// The media cannot be written (e.g., because of dust or a scratch) + WriteError = 706, + /// The media is write-protected or is of a not writable type. + MediaNotWriteable = 707, + /// The storage format of the currently loaded media is not supported for recording by this + /// device + RecordingFormatNotSupported = 708, + /// There is no free space left on the loaded media + MediaFull = 709, + /// The specified seek mode is not supported by the device + SeekModeNotSupported = 710, + /// The specified seek target is not specified in terms of the seek mode, or is not present on + /// the media + IllegalSeekTarget = 711, + /// The specified play mode is not supported by the device + PlayModeNotSupported = 712, + /// The specified record quality is not supported by the device + RecordQualityNotSupported = 713, + /// The resource to be played has a mimetype which is not supported by the AVTransport service + IllegalMimeType = 714, + /// This indicates the resource is already being played by other means + ContentBusy = 715, + /// The specified playback speed is not supported by the AVTransport service + PlaySpeedNotSupported = 717, + /// The specified instanceID is invalid for this AVTransport + InvalidInstanceId = 718, + /// The DNS Server is not available (HTTP error 503) + NoDnsServer = 737, + /// Unable to resolve the Fully Qualified Domain Name. (HTTP error 502) + BadDomainName = 738, + /// The server that hosts the resource is unreachable or unresponsive (HTTP error 404/410). + ServerError = 739, + /// Error we've not come across before + Unknown, +} + +impl From for AVTransportError { + fn from(code: u64) -> AVTransportError { + match code { + 401 => AVTransportError::InvalidAction, + 402 => AVTransportError::InvalidArgs, + 404 => AVTransportError::InvalidVar, + 501 => AVTransportError::ActionFailed, + 701 => AVTransportError::TransitionNotAvailable, + 702 => AVTransportError::NoContents, + 703 => AVTransportError::ReadError, + 704 => AVTransportError::FormatNotSupported, + 705 => AVTransportError::TransportLocked, + 706 => AVTransportError::WriteError, + 707 => AVTransportError::MediaNotWriteable, + 708 => AVTransportError::RecordingFormatNotSupported, + 709 => AVTransportError::MediaFull, + 710 => AVTransportError::SeekModeNotSupported, + 711 => AVTransportError::IllegalSeekTarget, + 712 => AVTransportError::PlayModeNotSupported, + 713 => AVTransportError::RecordQualityNotSupported, + 714 => AVTransportError::IllegalMimeType, + 715 => AVTransportError::ContentBusy, + 717 => AVTransportError::PlaySpeedNotSupported, + 718 => AVTransportError::InvalidInstanceId, + 737 => AVTransportError::NoDnsServer, + 738 => AVTransportError::BadDomainName, + 739 => AVTransportError::ServerError, + _ => AVTransportError::Unknown, + } + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index db9aff4..39f1c3c 100644 --- a/tests/integration_test.rs +++ a/tests/integration_test.rs @@ -74,6 +74,13 @@ } #[test] +fn seek() { + let device = get_speaker(); + device.seek(&std::time::Duration::from_secs(30)).expect("Failed to seek to 30 seconds"); + assert_eq!(device.track().expect("Failed to get track info").running_time.as_secs(), 30); +} + +#[test] fn play() { let device = get_speaker(); device.play().expect("Failed to play"); -- rgit 0.1.3