🏡 index : ~doyle/sonos.rs.git

author Jordan Doyle <jordan@doyle.wf> 2017-12-09 13:41:20.0 +00:00:00
committer Jordan Doyle <jordan@doyle.wf> 2017-12-09 13:41:20.0 +00:00:00
commit
f51621fe609cd2d23ab9395d1efdd5703fc6e2d6 [patch]
tree
77b1504e5c955f9b0664c51689efe2e3136f4b39
parent
31fd2791a48de5bbbbe343c8b4ff0b43f49dde32
download
f51621fe609cd2d23ab9395d1efdd5703fc6e2d6.tar.gz

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(-)

diff --git a/README.md b/README.md
index 67aee95..48a29f3 100644
--- a/README.md
+++ a/README.md
@@ -13,5 +13,3 @@
    device.play();

}
```


If your rooms change you have to rediscover.
diff --git a/src/device.rs b/src/device.rs
index febb5c3..66f779e 100644
--- a/src/device.rs
+++ a/src/device.rs
@@ -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");
}

/// Get the text of the given element as a String

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);

    // clean up xml so xmltree can read it
    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.")?;

    // get the group identifier from the given player
    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")?),
            // we slice the UDN to remove "uuid:"
            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(),
        })
    }

    /// Get the coordinator for this speaker.

    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);

        // clean up xml so xmltree can read it
        let content = content.replace(
            "<?xml-stylesheet type=\"text/xsl\" href=\"/xml/review.xsl\"?>",
            "",
        );

        // parse the topology xml
        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")?;

        // get the group identifier from the given player
        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()
                // get the coordinator for the given group
                .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")?)
    }

    /// Call the Sonos SOAP endpoint

@@ -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 @@
        )
    }

    // Play the current track
    /// Play the current track

    pub fn play(&self) -> Result<()> {
        self.soap(
            "MediaRenderer/AVTransport/Control",
@@ -202,7 +215,7 @@
        Ok(())
    }

    // Pause the current track
    /// Pause the current track

    pub fn pause(&self) -> Result<()> {
        self.soap(
            "MediaRenderer/AVTransport/Control",
@@ -215,7 +228,7 @@
        Ok(())
    }

    // Stop the current queue
    /// Stop the current queue

    pub fn stop(&self) -> Result<()> {
        self.soap(
            "MediaRenderer/AVTransport/Control",
@@ -228,7 +241,7 @@
        Ok(())
    }

    // Skip the current track
    /// Skip the current track

    pub fn next(&self) -> Result<()> {
        self.soap(
            "MediaRenderer/AVTransport/Control",
@@ -241,7 +254,7 @@
        Ok(())
    }

    // Go to the previous track
    /// Go to the previous track

    pub fn previous(&self) -> Result<()> {
        self.soap(
            "MediaRenderer/AVTransport/Control",
@@ -254,7 +267,7 @@
        Ok(())
    }

    // Seek to a time on the current track
    /// Seek to a time on the current track

    pub fn seek(&self, hours: &u8, minutes: &u8, seconds: &u8) -> Result<()> {
        self.soap(
            "MediaRenderer/AVTransport/Control",
@@ -272,7 +285,7 @@
        Ok(())
    }

    // Change the track, beginning at 1
    /// Change the track, beginning at 1

    pub fn play_queue_item(&self, track: &u64) -> Result<()> {
        self.soap(
            "MediaRenderer/AVTransport/Control",
@@ -288,7 +301,7 @@
        Ok(())
    }

    // Remove track at index from queue, beginning at 1
    /// Remove track at index from queue, beginning at 1

    pub fn remove_track(&self, track: &u64) -> Result<()> {
        self.soap(
            "MediaRenderer/AVTransport/Control",
@@ -304,12 +317,12 @@
        Ok(())
    }

    // Add a new track to the end of the queue
    /// 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",
            "RemoveTrackFromQueue",
            "AddURIToQueue",
            &format!(
                r#"
                  <InstanceID>0</InstanceID>
@@ -317,27 +330,6 @@
                  <EnqueuedURIMetaData></EnqueuedURIMetaData>
                  <DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued>
                  <EnqueueAsNext>0</EnqueueAsNext>"#,
                uri
            ),
            true,
        )?;

        Ok(())
    }

    // Add a track to the queue to play next
    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(())
    }

    // Replace the current track with a new one
    /// 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",
            "RemoveTrackFromQueue",
            "SetAVTransportURI",
            &format!(
                r#"
                  <InstanceID>0</InstanceID>
@@ -365,7 +357,7 @@
        Ok(())
    }

    // Remove every track from the queue
    /// Remove every track from the queue

    pub fn clear_queue(&self) -> Result<()> {
        self.soap(
            "MediaRenderer/AVTransport/Control",
@@ -378,7 +370,7 @@
        Ok(())
    }

    // Get the current volume
    /// Get the current volume

    pub fn volume(&self) -> Result<u8> {
        let res = self.soap(
            "MediaRenderer/RenderingControl/Control",
@@ -399,7 +391,7 @@
        Ok(volume)
    }

    // Set a new volume from 0-100.
    /// 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);
@@ -421,7 +413,7 @@
        Ok(())
    }

    // Check if this player is currently muted
    /// Check if this player is currently muted

    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,
        })
    }

    // Mute the current player
    /// Mute the current player

    pub fn mute(&self) -> Result<()> {
        self.soap(
            "MediaRenderer/RenderingControl/Control",
@@ -456,7 +446,7 @@
        Ok(())
    }

    // Unmute the current player
    /// Unmute the current player

    pub fn unmute(&self) -> Result<()> {
        self.soap(
            "MediaRenderer/RenderingControl/Control",
@@ -469,7 +459,7 @@
        Ok(())
    }

    // Get the transport state of the current player
    /// Get the transport state of the current player

    pub fn transport_state(&self) -> Result<TransportState> {
        let resp = self.soap(
            "MediaRenderer/AVTransport/Control",
@@ -493,7 +483,7 @@
        )
    }

    // Get information about the current track
    /// Get information about the current track

    pub fn track(&self) -> Result<Track> {
        let resp = self.soap(
            "MediaRenderer/AVTransport/Control",
diff --git a/src/discovery.rs b/src/discovery.rs
index 0db43cb..35d5ecc 100644
--- a/src/discovery.rs
+++ a/src/discovery.rs
@@ -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";

/// Convenience method to grab a header from an SSDP search as a string.

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")
}

/// Discover all speakers on the current network.

///

/// This method **will** block for 2 seconds while waiting for broadcast responses.

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)
diff --git a/src/lib.rs b/src/lib.rs
index e28d902..0025c3a 100644
--- a/src/lib.rs
+++ a/src/lib.rs
@@ -19,10 +19,4 @@
pub use device::TransportState;
pub use error::*;

/// Discover devices.

///

/// You should only run this function once. It will block for

/// 2 seconds while it scans.

pub fn discover() -> Result<Vec<Speaker>> {
    discovery::discover()
}
pub use discovery::discover;
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index 4acc6b5..ff24328 100644
--- a/tests/integration_test.rs
+++ a/tests/integration_test.rs
@@ -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");
}