Преглед изворни кода

Merge branch 'master' into error-source-fix

Sasha Hilton пре 6 година
родитељ
комит
1ccf00cfbb

Разлика између датотеке није приказан због своје велике величине
+ 414 - 115
Cargo.lock


+ 4 - 8
Cargo.toml

@@ -43,35 +43,31 @@ num-bigint = "0.1.35"
 protobuf = "1.1"
 rand = "0.6"
 rpassword = "0.3.0"
-rust-crypto = "0.2.36"
-serde = "0.9.6"
-serde_derive = "0.9.6"
-serde_json = "0.9.5"
 tokio-core = "0.1.2"
 tokio-io = "0.1"
 tokio-process = "0.2.2"
 tokio-signal = "0.1.2"
 url = "1.7.0"
+sha-1 = "0.8.0"
+hex = "0.3.2"
 
 [build-dependencies]
 rand            = "0.6"
 vergen          = "0.1.0"
 
-[replace]
-"rust-crypto:0.2.36" = { git = "https://github.com/awmath/rust-crypto.git", branch = "avx2" }
-
 [features]
 alsa-backend = ["librespot-playback/alsa-backend"]
 portaudio-backend = ["librespot-playback/portaudio-backend"]
 pulseaudio-backend = ["librespot-playback/pulseaudio-backend"]
 jackaudio-backend = ["librespot-playback/jackaudio-backend"]
+rodio-backend = ["librespot-playback/rodio-backend"]
 
 with-tremor = ["librespot-audio/with-tremor"]
 with-vorbis = ["librespot-audio/with-vorbis"]
 
 with-dns-sd = ["librespot-connect/with-dns-sd"]
 
-default = ["librespot-playback/portaudio-backend"]
+default = ["librespot-playback/rodio-backend"]
 
 [package.metadata.deb]
 maintainer = "librespot-org"

+ 1 - 1
audio/Cargo.toml

@@ -14,8 +14,8 @@ lewton = "0.9.3"
 log = "0.3.5"
 num-bigint = "0.1.35"
 num-traits = "0.1.36"
-rust-crypto = "0.2.36"
 tempfile = "2.1"
+aes-ctr = "0.3.0"
 
 tremor = { git = "https://github.com/plietar/rust-tremor", optional = true }
 vorbis = { version ="0.1.0", optional = true }

+ 19 - 28
audio/src/decrypt.rs

@@ -1,39 +1,38 @@
-use crypto::aes;
-use crypto::symmetriccipher::SynchronousStreamCipher;
-use num_bigint::BigUint;
-use num_traits::FromPrimitive;
 use std::io;
-use std::ops::Add;
+
+use aes_ctr::Aes128Ctr;
+use aes_ctr::stream_cipher::{
+    NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek
+};
+use aes_ctr::stream_cipher::generic_array::GenericArray;
 
 use core::audio_key::AudioKey;
 
-const AUDIO_AESIV: &'static [u8] = &[
-    0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93,
+const AUDIO_AESIV: [u8; 16] = [
+    0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77,
+    0xeb, 0xe8, 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93,
 ];
 
 pub struct AudioDecrypt<T: io::Read> {
-    cipher: Box<SynchronousStreamCipher + 'static>,
-    key: AudioKey,
+    cipher: Aes128Ctr,
     reader: T,
 }
 
 impl<T: io::Read> AudioDecrypt<T> {
     pub fn new(key: AudioKey, reader: T) -> AudioDecrypt<T> {
-        let cipher = aes::ctr(aes::KeySize::KeySize128, &key.0, AUDIO_AESIV);
-        AudioDecrypt {
-            cipher: cipher,
-            key: key,
-            reader: reader,
-        }
+        let cipher = Aes128Ctr::new(
+            &GenericArray::from_slice(&key.0),
+            &GenericArray::from_slice(&AUDIO_AESIV),
+        );
+        AudioDecrypt { cipher, reader }
     }
 }
 
 impl<T: io::Read> io::Read for AudioDecrypt<T> {
     fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
-        let mut buffer = vec![0u8; output.len()];
-        let len = try!(self.reader.read(&mut buffer));
+        let len = try!(self.reader.read(output));
 
-        self.cipher.process(&buffer[..len], &mut output[..len]);
+        self.cipher.apply_keystream(&mut output[..len]);
 
         Ok(len)
     }
@@ -42,17 +41,9 @@ impl<T: io::Read> io::Read for AudioDecrypt<T> {
 impl<T: io::Read + io::Seek> io::Seek for AudioDecrypt<T> {
     fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
         let newpos = try!(self.reader.seek(pos));
-        let skip = newpos % 16;
-
-        let iv = BigUint::from_bytes_be(AUDIO_AESIV)
-            .add(BigUint::from_u64(newpos / 16).unwrap())
-            .to_bytes_be();
-        self.cipher = aes::ctr(aes::KeySize::KeySize128, &self.key.0, &iv);
 
-        let buf = vec![0u8; skip as usize];
-        let mut buf2 = vec![0u8; skip as usize];
-        self.cipher.process(&buf, &mut buf2);
+        self.cipher.seek(newpos);
 
-        Ok(newpos as u64)
+        Ok(newpos)
     }
 }

+ 1 - 1
audio/src/lib.rs

@@ -5,10 +5,10 @@ extern crate log;
 
 extern crate bit_set;
 extern crate byteorder;
-extern crate crypto;
 extern crate num_bigint;
 extern crate num_traits;
 extern crate tempfile;
+extern crate aes_ctr;
 
 extern crate librespot_core as core;
 

+ 7 - 4
connect/Cargo.toml

@@ -18,12 +18,15 @@ log = "0.3.5"
 num-bigint = "0.1.35"
 protobuf = "2.0.5"
 rand = "0.6"
-rust-crypto = "0.2.36"
-serde = "0.9.6"
-serde_derive = "0.9.6"
-serde_json = "0.9.5"
+serde = "1.0"
+serde_derive = "1.0"
+serde_json = "1.0"
 tokio-core = "0.1.2"
 url = "1.3"
+sha-1 = "0.8.0"
+hmac = "0.7.0"
+aes-ctr = "0.3.0"
+block-modes = "0.2.0"
 
 dns-sd = { version = "0.1.3", optional = true }
 mdns = { git = "https://github.com/plietar/rust-mdns", optional = true }

+ 86 - 0
connect/src/context.rs

@@ -0,0 +1,86 @@
+use core::spotify_id::SpotifyId;
+use protocol::spirc::TrackRef;
+
+use serde;
+
+#[derive(Deserialize, Debug)]
+pub struct StationContext {
+    pub uri: Option<String>,
+    pub next_page_url: String,
+    #[serde(deserialize_with = "deserialize_protobuf_TrackRef")]
+    pub tracks: Vec<TrackRef>,
+    // Not required for core functionality
+    // pub seeds: Vec<String>,
+    // #[serde(rename = "imageUri")]
+    // pub image_uri: String,
+    // pub subtitle: Option<String>,
+    // pub subtitles: Vec<String>,
+    // #[serde(rename = "subtitleUri")]
+    // pub subtitle_uri: Option<String>,
+    // pub title: String,
+    // #[serde(rename = "titleUri")]
+    // pub title_uri: String,
+    // pub related_artists: Vec<ArtistContext>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct PageContext {
+    pub uri: String,
+    pub next_page_url: String,
+    #[serde(deserialize_with = "deserialize_protobuf_TrackRef")]
+    pub tracks: Vec<TrackRef>,
+    // Not required for core functionality
+    // pub url: String,
+    // // pub restrictions:
+}
+
+#[derive(Deserialize, Debug)]
+pub struct TrackContext {
+    #[serde(rename = "original_gid")]
+    pub gid: String,
+    pub uri: String,
+    pub uid: String,
+    // Not required for core functionality
+    // pub album_uri: String,
+    // pub artist_uri: String,
+    // pub metadata: MetadataContext,
+}
+
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct ArtistContext {
+    artist_name: String,
+    artist_uri: String,
+    image_uri: String,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct MetadataContext {
+    album_title: String,
+    artist_name: String,
+    artist_uri: String,
+    image_url: String,
+    title: String,
+    uid: String,
+}
+
+#[allow(non_snake_case)]
+fn deserialize_protobuf_TrackRef<'d, D>(de: D) -> Result<Vec<TrackRef>, D::Error>
+where
+    D: serde::Deserializer<'d>,
+{
+    let v: Vec<TrackContext> = try!(serde::Deserialize::deserialize(de));
+    let track_vec = v
+        .iter()
+        .map(|v| {
+            let mut t = TrackRef::new();
+            //  This has got to be the most round about way of doing this.
+            t.set_gid(SpotifyId::from_base62(&v.gid).unwrap().to_raw().to_vec());
+            t.set_uri(v.uri.to_owned());
+
+            t
+        })
+        .collect::<Vec<TrackRef>>();
+
+    Ok(track_vec)
+}

+ 36 - 26
connect/src/discovery.rs

@@ -1,7 +1,9 @@
 use base64;
-use crypto;
-use crypto::digest::Digest;
-use crypto::mac::Mac;
+use sha1::{Sha1, Digest};
+use hmac::{Hmac, Mac};
+use aes_ctr::Aes128Ctr;
+use aes_ctr::stream_cipher::{NewStreamCipher, SyncStreamCipher};
+use aes_ctr::stream_cipher::generic_array::GenericArray;
 use futures::sync::mpsc;
 use futures::{Future, Poll, Stream};
 use hyper::server::{Http, Request, Response, Service};
@@ -26,6 +28,8 @@ use core::config::ConnectConfig;
 use core::diffie_hellman::{DH_GENERATOR, DH_PRIME};
 use core::util;
 
+type HmacSha1 = Hmac<Sha1>;
+
 #[derive(Clone)]
 struct Discovery(Arc<DiscoveryInner>);
 struct DiscoveryInner {
@@ -106,39 +110,45 @@ impl Discovery {
         let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20];
         let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()];
 
-        let base_key = {
-            let mut data = [0u8; 20];
-            let mut h = crypto::sha1::Sha1::new();
-            h.input(&shared_key.to_bytes_be());
-            h.result(&mut data);
-            data[..16].to_owned()
-        };
+        let base_key = Sha1::digest(&shared_key.to_bytes_be());
+        let base_key = &base_key[..16];
 
         let checksum_key = {
-            let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &base_key);
+            let mut h = HmacSha1::new_varkey(base_key)
+                .expect("HMAC can take key of any size");
             h.input(b"checksum");
-            h.result().code().to_owned()
+            h.result().code()
         };
 
         let encryption_key = {
-            let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &base_key);
+            let mut h = HmacSha1::new_varkey(&base_key)
+                .expect("HMAC can take key of any size");
             h.input(b"encryption");
-            h.result().code().to_owned()
+            h.result().code()
         };
 
-        let mac = {
-            let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &checksum_key);
-            h.input(encrypted);
-            h.result().code().to_owned()
-        };
-
-        assert_eq!(&mac[..], cksum);
+        let mut h = HmacSha1::new_varkey(&checksum_key)
+            .expect("HMAC can take key of any size");
+        h.input(encrypted);
+        if let Err(_) = h.verify(cksum) {
+            warn!("Login error for user {:?}: MAC mismatch", username);
+            let result = json!({
+                "status": 102,
+                "spotifyError": 1,
+                "statusString": "ERROR-MAC"
+            });
+
+            let body = result.to_string();
+            return ::futures::finished(Response::new().with_body(body))
+        }
 
         let decrypted = {
-            let mut data = vec![0u8; encrypted.len()];
-            let mut cipher =
-                crypto::aes::ctr(crypto::aes::KeySize::KeySize128, &encryption_key[0..16], iv);
-            cipher.process(encrypted, &mut data);
+            let mut data = encrypted.to_vec();
+            let mut cipher = Aes128Ctr::new(
+                &GenericArray::from_slice(&encryption_key[0..16]),
+                &GenericArray::from_slice(iv),
+            );
+            cipher.apply_keystream(&mut data);
             String::from_utf8(data).unwrap()
         };
 
@@ -221,7 +231,6 @@ pub fn discovery(
 
     let serve = {
         let http = Http::new();
-        debug!("Zeroconf server listening on 0.0.0.0:{}", port);
         http.serve_addr_handle(
             &format!("0.0.0.0:{}", port).parse().unwrap(),
             &handle,
@@ -230,6 +239,7 @@ pub fn discovery(
     };
 
     let s_port = serve.incoming_ref().local_addr().port();
+    debug!("Zeroconf server listening on 0.0.0.0:{}", s_port);
 
     let server_future = {
         let handle = handle.clone();

+ 9 - 1
connect/src/lib.rs

@@ -2,9 +2,11 @@
 extern crate log;
 #[macro_use]
 extern crate serde_json;
+#[macro_use]
+extern crate serde_derive;
+extern crate serde;
 
 extern crate base64;
-extern crate crypto;
 extern crate futures;
 extern crate hyper;
 extern crate num_bigint;
@@ -13,6 +15,11 @@ extern crate rand;
 extern crate tokio_core;
 extern crate url;
 
+extern crate sha1;
+extern crate hmac;
+extern crate aes_ctr;
+extern crate block_modes;
+
 #[cfg(feature = "with-dns-sd")]
 extern crate dns_sd;
 
@@ -23,5 +30,6 @@ extern crate librespot_core as core;
 extern crate librespot_playback as playback;
 extern crate librespot_protocol as protocol;
 
+pub mod context;
 pub mod discovery;
 pub mod spirc;

+ 135 - 6
connect/src/spirc.rs

@@ -16,7 +16,9 @@ use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State};
 
 use playback::mixer::Mixer;
 use playback::player::Player;
+use serde_json;
 
+use context::StationContext;
 use rand;
 use rand::seq::SliceRandom;
 use std;
@@ -40,6 +42,8 @@ pub struct SpircTask {
 
     shutdown: bool,
     session: Session,
+    context_fut: Box<Future<Item = serde_json::Value, Error = MercuryError>>,
+    context: Option<StationContext>,
 }
 
 pub enum SpircCommand {
@@ -53,6 +57,9 @@ pub enum SpircCommand {
     Shutdown,
 }
 
+const CONTEXT_TRACKS_HISTORY: usize = 10;
+const CONTEXT_FETCH_THRESHOLD: u32 = 5;
+
 pub struct Spirc {
     commands: mpsc::UnboundedSender<SpircCommand>,
 }
@@ -139,6 +146,15 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState {
                 };
                 msg
             };
+            {
+                let msg = repeated.push_default();
+                msg.set_typ(protocol::spirc::CapabilityType::kSupportsPlaylistV2);
+                {
+                    let repeated = msg.mut_intValue();
+                    repeated.push(64)
+                };
+                msg
+            };
             {
                 let msg = repeated.push_default();
                 msg.set_typ(protocol::spirc::CapabilityType::kSupportedContexts);
@@ -176,7 +192,7 @@ fn calc_logarithmic_volume(volume: u16) -> u16 {
     // Volume conversion taken from https://www.dr-lex.be/info-stuff/volumecontrols.html#ideal2
     // Convert the given volume [0..0xffff] to a dB gain
     // We assume a dB range of 60dB.
-    // Use the equatation: a * exp(b * x)
+    // Use the equation: a * exp(b * x)
     // in which a = IDEAL_FACTOR, b = 1/1000
     const IDEAL_FACTOR: f64 = 6.908;
     let normalized_volume = volume as f64 / std::u16::MAX as f64; // To get a value between 0 and 1
@@ -259,6 +275,9 @@ impl Spirc {
 
             shutdown: false,
             session: session.clone(),
+
+            context_fut: Box::new(future::empty()),
+            context: None,
         };
 
         task.set_volume(volume);
@@ -335,6 +354,39 @@ impl Future for SpircTask {
                     Ok(Async::NotReady) => (),
                     Err(oneshot::Canceled) => self.end_of_track = Box::new(future::empty()),
                 }
+
+                match self.context_fut.poll() {
+                    Ok(Async::Ready(value)) => {
+                        let r_context = serde_json::from_value::<StationContext>(value.clone());
+                        self.context = match r_context {
+                            Ok(context) => {
+                                info!(
+                                    "Resolved {:?} tracks from <{:?}>",
+                                    context.tracks.len(),
+                                    self.state.get_context_uri(),
+                                );
+                                Some(context)
+                            }
+                            Err(e) => {
+                                error!("Unable to parse JSONContext {:?}\n{:?}", e, value);
+                                None
+                            }
+                        };
+                        // It needn't be so verbose - can be as simple as
+                        // if let Some(ref context) = r_context {
+                        //     info!("Got {:?} tracks from <{}>", context.tracks.len(), context.uri);
+                        // }
+                        // self.context = r_context;
+
+                        progress = true;
+                        self.context_fut = Box::new(future::empty());
+                    }
+                    Ok(Async::NotReady) => (),
+                    Err(err) => {
+                        self.context_fut = Box::new(future::empty());
+                        error!("ContextError: {:?}", err)
+                    }
+                }
             }
 
             let poll_sender = self.sender.poll_complete().unwrap();
@@ -455,6 +507,7 @@ impl SpircTask {
                     let play = frame.get_state().get_status() == PlayStatus::kPlayStatusPlay;
                     self.load_track(play);
                 } else {
+                    info!("No more tracks left in queue");
                     self.state.set_status(PlayStatus::kPlayStatusStop);
                 }
 
@@ -600,6 +653,21 @@ impl SpircTask {
     fn handle_next(&mut self) {
         let mut new_index = self.consume_queued_track() as u32;
         let mut continue_playing = true;
+        debug!(
+            "At track {:?} of {:?} <{:?}> update [{}]",
+            new_index,
+            self.state.get_track().len(),
+            self.state.get_context_uri(),
+            self.state.get_track().len() as u32 - new_index < CONTEXT_FETCH_THRESHOLD
+        );
+        let context_uri = self.state.get_context_uri().to_owned();
+        if (context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:"))
+            && ((self.state.get_track().len() as u32) - new_index) < CONTEXT_FETCH_THRESHOLD
+        {
+            self.context_fut = self.resolve_station(&context_uri);
+            self.update_tracks_from_context();
+        }
+
         if new_index >= self.state.get_track().len() as u32 {
             new_index = 0; // Loop around back to start
             continue_playing = self.state.get_repeat();
@@ -680,10 +748,59 @@ impl SpircTask {
         self.state.get_position_ms() + diff as u32
     }
 
+    fn resolve_station(&self, uri: &str) -> Box<Future<Item = serde_json::Value, Error = MercuryError>> {
+        let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri);
+
+        self.resolve_uri(&radio_uri)
+    }
+
+    fn resolve_uri(&self, uri: &str) -> Box<Future<Item = serde_json::Value, Error = MercuryError>> {
+        let request = self.session.mercury().get(uri);
+
+        Box::new(request.and_then(move |response| {
+            let data = response.payload.first().expect("Empty payload on context uri");
+            let response: serde_json::Value = serde_json::from_slice(&data).unwrap();
+
+            Ok(response)
+        }))
+    }
+
+    fn update_tracks_from_context(&mut self) {
+        if let Some(ref context) = self.context {
+            self.context_fut = self.resolve_uri(&context.next_page_url);
+
+            let new_tracks = &context.tracks;
+            debug!("Adding {:?} tracks from context to playlist", new_tracks.len());
+            let current_index = self.state.get_playing_track_index();
+            let mut new_index = 0;
+            {
+                let mut tracks = self.state.mut_track();
+                // Does this need to be optimised - we don't need to actually traverse the len of tracks
+                let tracks_len = tracks.len();
+                if tracks_len > CONTEXT_TRACKS_HISTORY {
+                    tracks.rotate_right(tracks_len - CONTEXT_TRACKS_HISTORY);
+                    tracks.truncate(CONTEXT_TRACKS_HISTORY);
+                }
+                // tracks.extend_from_slice(&mut new_tracks); // method doesn't exist for protobuf::RepeatedField
+                for t in new_tracks {
+                    tracks.push(t.to_owned());
+                }
+                if current_index > CONTEXT_TRACKS_HISTORY as u32 {
+                    new_index = current_index - CONTEXT_TRACKS_HISTORY as u32;
+                }
+            }
+            self.state.set_playing_track_index(new_index);
+        }
+    }
+
     fn update_tracks(&mut self, frame: &protocol::spirc::Frame) {
         let index = frame.get_state().get_playing_track_index();
-        let tracks = frame.get_state().get_track();
         let context_uri = frame.get_state().get_context_uri().to_owned();
+        let tracks = frame.get_state().get_track();
+        debug!("Frame has {:?} tracks", tracks.len());
+        if context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:") {
+            self.context_fut = self.resolve_station(&context_uri);
+        }
 
         self.state.set_playing_track_index(index);
         self.state.set_track(tracks.into_iter().cloned().collect());
@@ -693,13 +810,25 @@ impl SpircTask {
     }
 
     fn load_track(&mut self, play: bool) {
-        let index = self.state.get_playing_track_index();
         let track = {
-            let gid = self.state.get_track()[index as usize].get_gid();
-            SpotifyId::from_raw(gid).unwrap()
+            let mut index = self.state.get_playing_track_index();
+            // Check for malformed gid
+            let tracks_len = self.state.get_track().len() as u32;
+            let mut track_ref = &self.state.get_track()[index as usize];
+            while track_ref.get_gid().len() != 16 {
+                warn!(
+                    "Skipping track {:?} at position [{}] of {}",
+                    track_ref.get_uri(),
+                    index,
+                    tracks_len
+                );
+                index = if index + 1 < tracks_len { index + 1 } else { 0 };
+                track_ref = &self.state.get_track()[index as usize];
+            }
+            SpotifyId::from_raw(track_ref.get_gid()).unwrap()
         };
-        let position = self.state.get_position_ms();
 
+        let position = self.state.get_position_ms();
         let end_of_track = self.player.load(track, play, position);
 
         if play {

+ 7 - 4
core/Cargo.toml

@@ -25,16 +25,19 @@ num-traits = "0.1.36"
 protobuf = "2.0.5"
 rand = "0.6"
 rpassword = "0.3.0"
-rust-crypto = "0.2.36"
-serde = "0.9.6"
-serde_derive = "0.9.6"
-serde_json = "0.9.5"
+serde = "1.0"
+serde_derive = "1.0"
+serde_json = "1.0"
 shannon = "0.2.0"
 tokio-codec = "0.1.1"
 tokio-core = "0.1.2"
 tokio-io = "0.1"
 url = "1.7.0"
 uuid = { version = "0.4", features = ["v4"] }
+sha-1 = "0.8.0"
+hmac = "0.7.0"
+pbkdf2 = "0.3.0"
+aes = "0.3.0"
 
 [build-dependencies]
 rand = "0.6"

+ 31 - 41
core/src/authentication.rs

@@ -1,11 +1,9 @@
 use base64;
 use byteorder::{BigEndian, ByteOrder};
-use crypto;
-use crypto::aes;
-use crypto::digest::Digest;
-use crypto::hmac::Hmac;
-use crypto::pbkdf2::pbkdf2;
-use crypto::sha1::Sha1;
+use aes::Aes192;
+use hmac::Hmac;
+use sha1::{Sha1, Digest};
+use pbkdf2::pbkdf2;
 use protobuf::ProtobufEnum;
 use serde;
 use serde_json;
@@ -63,42 +61,34 @@ impl Credentials {
             Ok(data)
         }
 
-        let encrypted_blob = base64::decode(encrypted_blob).unwrap();
-
-        let secret = {
-            let mut data = [0u8; 20];
-            let mut h = crypto::sha1::Sha1::new();
-            h.input(device_id.as_bytes());
-            h.result(&mut data);
-            data
-        };
+        let secret = Sha1::digest(device_id.as_bytes());
 
         let key = {
-            let mut data = [0u8; 24];
-            let mut mac = Hmac::new(Sha1::new(), &secret);
-            pbkdf2(&mut mac, username.as_bytes(), 0x100, &mut data[0..20]);
-
-            let mut hash = Sha1::new();
-            hash.input(&data[0..20]);
-            hash.result(&mut data[0..20]);
-            BigEndian::write_u32(&mut data[20..], 20);
-            data
+            let mut key = [0u8; 24];
+            pbkdf2::<Hmac<Sha1>>(&secret, username.as_bytes(), 0x100, &mut key[0..20]);
+
+            let hash = &Sha1::digest(&key[..20]);
+            key[..20].copy_from_slice(hash);
+            BigEndian::write_u32(&mut key[20..], 20);
+            key
         };
 
+        // decrypt data using ECB mode without padding
         let blob = {
-            // Anyone know what this block mode is ?
-            let mut data = vec![0u8; encrypted_blob.len()];
-            let mut cipher =
-                aes::ecb_decryptor(aes::KeySize::KeySize192, &key, crypto::blockmodes::NoPadding);
-            cipher
-                .decrypt(
-                    &mut crypto::buffer::RefReadBuffer::new(&encrypted_blob),
-                    &mut crypto::buffer::RefWriteBuffer::new(&mut data),
-                    true,
-                )
-                .unwrap();
-
-            let l = encrypted_blob.len();
+            use aes::block_cipher_trait::BlockCipher;
+            use aes::block_cipher_trait::generic_array::GenericArray;
+            use aes::block_cipher_trait::generic_array::typenum::Unsigned;
+
+            let mut data = base64::decode(encrypted_blob).unwrap();
+            let cipher = Aes192::new(GenericArray::from_slice(&key));
+            let block_size = <Aes192 as BlockCipher>::BlockSize::to_usize();
+            assert_eq!(data.len() % block_size, 0);
+            // replace to chunks_exact_mut with MSRV bump to 1.31
+            for chunk in data.chunks_mut(block_size) {
+                cipher.decrypt_block(GenericArray::from_mut_slice(chunk));
+            }
+
+            let l = data.len();
             for i in 0..l - 0x10 {
                 data[l - i - 1] ^= data[l - i - 0x11];
             }
@@ -152,10 +142,10 @@ where
     serde::Serialize::serialize(&v.value(), ser)
 }
 
-fn deserialize_protobuf_enum<T, D>(de: D) -> Result<T, D::Error>
+fn deserialize_protobuf_enum<'de, T, D>(de: D) -> Result<T, D::Error>
 where
     T: ProtobufEnum,
-    D: serde::Deserializer,
+    D: serde::Deserializer<'de>,
 {
     let v: i32 = try!(serde::Deserialize::deserialize(de));
     T::from_i32(v).ok_or_else(|| serde::de::Error::custom("Invalid enum value"))
@@ -169,9 +159,9 @@ where
     serde::Serialize::serialize(&base64::encode(v.as_ref()), ser)
 }
 
-fn deserialize_base64<D>(de: D) -> Result<Vec<u8>, D::Error>
+fn deserialize_base64<'de, D>(de: D) -> Result<Vec<u8>, D::Error>
 where
-    D: serde::Deserializer,
+    D: serde::Deserializer<'de>,
 {
     let v: String = try!(serde::Deserialize::deserialize(de));
     base64::decode(&v).map_err(|e| serde::de::Error::custom(e.to_string()))

+ 9 - 8
core/src/connection/handshake.rs

@@ -1,7 +1,6 @@
 use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
-use crypto::hmac::Hmac;
-use crypto::mac::Mac;
-use crypto::sha1::Sha1;
+use hmac::{Hmac, Mac};
+use sha1::Sha1;
 use futures::{Async, Future, Poll};
 use protobuf::{self, Message};
 use rand::thread_rng;
@@ -89,7 +88,7 @@ fn client_hello<T: AsyncWrite>(connection: T, gc: Vec<u8>) -> WriteAll<T, Vec<u8
     packet
         .mut_build_info()
         .set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86);
-    packet.mut_build_info().set_version(0x10800000000);
+    packet.mut_build_info().set_version(109800078);
     packet
         .mut_cryptosuites_supported()
         .push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON);
@@ -187,17 +186,19 @@ fn read_into_accumulator<T: AsyncRead>(
 }
 
 fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
-    let mut data = Vec::with_capacity(0x64);
-    let mut mac = Hmac::new(Sha1::new(), &shared_secret);
+    type HmacSha1 = Hmac<Sha1>;
 
+    let mut data = Vec::with_capacity(0x64);
     for i in 1..6 {
+        let mut mac = HmacSha1::new_varkey(&shared_secret)
+            .expect("HMAC can take key of any size");
         mac.input(packets);
         mac.input(&[i]);
         data.extend_from_slice(&mac.result().code());
-        mac.reset();
     }
 
-    mac = Hmac::new(Sha1::new(), &data[..0x14]);
+    let mut mac = HmacSha1::new_varkey(&data[..0x14])
+        .expect("HMAC can take key of any size");;
     mac.input(packets);
 
     (

+ 4 - 1
core/src/lib.rs

@@ -14,7 +14,6 @@ extern crate serde_derive;
 extern crate base64;
 extern crate byteorder;
 extern crate bytes;
-extern crate crypto;
 extern crate extprim;
 extern crate httparse;
 extern crate hyper;
@@ -33,6 +32,10 @@ extern crate tokio_core;
 extern crate tokio_io;
 extern crate url;
 extern crate uuid;
+extern crate sha1;
+extern crate hmac;
+extern crate pbkdf2;
+extern crate aes;
 
 extern crate librespot_protocol as protocol;
 

+ 3 - 0
playback/Cargo.toml

@@ -20,9 +20,12 @@ portaudio-rs    = { version = "0.3.0", optional = true }
 libpulse-sys    = { version = "0.0.0", optional = true }
 jack            = { version = "0.5.3", optional = true }
 libc            = { version = "0.2", optional = true }
+rodio           = { git = "https://github.com/tomaka/rodio", optional = true, default-features = false }
+cpal            = { version = "*", optional = true }
 
 [features]
 alsa-backend = ["alsa"]
 portaudio-backend = ["portaudio-rs"]
 pulseaudio-backend = ["libpulse-sys", "libc"]
 jackaudio-backend = ["jack"]
+rodio-backend = ["rodio", "cpal"]

+ 7 - 0
playback/src/audio_backend/mod.rs

@@ -34,6 +34,11 @@ mod jackaudio;
 #[cfg(feature = "jackaudio-backend")]
 use self::jackaudio::JackSink;
 
+#[cfg(feature = "rodio-backend")]
+mod rodio;
+#[cfg(feature = "rodio-backend")]
+use self::rodio::RodioSink;
+
 mod pipe;
 use self::pipe::StdoutSink;
 
@@ -46,6 +51,8 @@ pub const BACKENDS: &'static [(&'static str, fn(Option<String>) -> Box<Sink>)] =
     ("pulseaudio", mk_sink::<PulseAudioSink>),
     #[cfg(feature = "jackaudio-backend")]
     ("jackaudio", mk_sink::<JackSink>),
+    #[cfg(feature = "rodio-backend")]
+    ("rodio", mk_sink::<RodioSink>),
     ("pipe", mk_sink::<StdoutSink>),
 ];
 

+ 115 - 0
playback/src/audio_backend/rodio.rs

@@ -0,0 +1,115 @@
+use super::{Open, Sink};
+extern crate rodio;
+extern crate cpal;
+use std::{io, thread, time};
+use std::process::exit;
+
+pub struct RodioSink {
+    rodio_sink: rodio::Sink,
+}
+
+fn list_formats(ref device: &rodio::Device) {
+    let default_fmt = match device.default_output_format() {
+        Ok(fmt) => cpal::SupportedFormat::from(fmt),
+        Err(e) => {
+            warn!("Error getting default rodio::Sink format: {:?}", e);
+            return;
+        },
+    };
+
+    let mut output_formats = match device.supported_output_formats() {
+        Ok(f) => f.peekable(),
+        Err(e) => {
+            warn!("Error getting supported rodio::Sink formats: {:?}", e);
+            return;
+        },
+    };
+
+    if output_formats.peek().is_some() {
+        debug!("  Available formats:");
+        for format in output_formats {
+            let s = format!("{}ch, {:?}, min {:?}, max {:?}", format.channels, format.data_type, format.min_sample_rate, format.max_sample_rate);
+            if format == default_fmt {
+                debug!("    (default) {}", s);
+            } else {
+                debug!("    {:?}", format);
+            }
+        }
+    }
+}
+
+fn list_outputs() {
+    let default_device = rodio::default_output_device().unwrap();
+    println!("Default Audio Device:\n  {}", default_device.name());
+    list_formats(&default_device);
+
+    println!("Other Available Audio Devices:");
+    for device in rodio::output_devices() {
+        if device.name() != default_device.name() {
+            println!("  {}", device.name());
+            list_formats(&device);
+        }
+    }
+}
+
+impl Open for RodioSink {
+    fn open(device: Option<String>) -> RodioSink {
+        debug!("Using rodio sink");
+
+        let mut rodio_device = rodio::default_output_device().expect("no output device available");
+        if device.is_some() {
+            let device_name = device.unwrap();
+
+            if device_name == "?".to_string() {
+                list_outputs();
+                exit(0)
+            }
+            let mut found = false;
+            for d in rodio::output_devices() {
+                if d.name() == device_name {
+                    rodio_device = d;
+                    found = true;
+                    break;
+                }
+            }
+            if !found {
+                println!("No output sink matching '{}' found.", device_name);
+                exit(0)
+            }
+        }
+        let sink = rodio::Sink::new(&rodio_device);
+
+        RodioSink {
+            rodio_sink: sink,
+        }
+    }
+}
+
+impl Sink for RodioSink {
+    fn start(&mut self) -> io::Result<()> {
+        // More similar to an "unpause" than "play". Doesn't undo "stop".
+        // self.rodio_sink.play();
+        Ok(())
+    }
+
+    fn stop(&mut self) -> io::Result<()> {
+        // This will immediately stop playback, but the sink is then unusable.
+        // We just have to let the current buffer play till the end.
+        // self.rodio_sink.stop();
+        Ok(())
+    }
+
+    fn write(&mut self, data: &[i16]) -> io::Result<()> {
+        let source = rodio::buffer::SamplesBuffer::new(2, 44100, data);
+        self.rodio_sink.append(source);
+
+        // Chunk sizes seem to be about 256 to 3000 ish items long.
+        // Assuming they're on average 1628 then a half second buffer is:
+        // 44100 elements --> about 27 chunks
+        while self.rodio_sink.len() > 26 {
+            // sleep and wait for rodio to drain a bit
+            thread::sleep(time::Duration::from_millis(10));
+        }
+        Ok(())
+    }
+}

+ 9 - 7
playback/src/player.rs

@@ -560,12 +560,12 @@ impl PlayerInternal {
         let key = self
             .session
             .audio_key()
-            .request(track.id, file_id)
-            .wait()
-            .unwrap();
+            .request(track.id, file_id);
+        let encrypted_file = AudioFile::open(&self.session, file_id);
 
-        let encrypted_file = AudioFile::open(&self.session, file_id).wait().unwrap();
 
+        let encrypted_file = encrypted_file.wait().unwrap();
+        let key = key.wait().unwrap();
         let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
 
         let normalisation_factor = match NormalisationData::parse_from_file(&mut decrypted_file) {
@@ -580,9 +580,11 @@ impl PlayerInternal {
 
         let mut decoder = VorbisDecoder::new(audio_file).unwrap();
 
-        match decoder.seek(position) {
-            Ok(_) => (),
-            Err(err) => error!("Vorbis error: {:?}", err),
+        if position != 0 {
+            match decoder.seek(position) {
+                Ok(_) => (),
+                Err(err) => error!("Vorbis error: {:?}", err),
+            }
         }
 
         info!("Track \"{}\" loaded", track.name);

+ 1 - 1
protocol/Cargo.toml

@@ -8,4 +8,4 @@ build = "build.rs"
 protobuf = "2.0.5"
 
 [build-dependencies]
-protoc-rust = "2.0.5"
+protobuf-codegen-pure = "2.0.5"

+ 3 - 4
protocol/build.rs

@@ -1,5 +1,5 @@
-extern crate protoc_rust;
-use protoc_rust::Customize;
+extern crate protobuf_codegen_pure;
+use protobuf_codegen_pure::Customize;
 use std::fs::File;
 use std::io::prelude::*;
 
@@ -14,7 +14,7 @@ fn main() {
     for &(path, expected_checksum) in files::FILES {
         let actual = cksum_file(path).unwrap();
         if expected_checksum != actual {
-            protoc_rust::run(protoc_rust::Args {
+            protobuf_codegen_pure::run(protobuf_codegen_pure::Args {
                 out_dir: "src",
                 input: &[path],
                 includes: &["proto"],
@@ -28,7 +28,6 @@ fn main() {
     if changed {
         // Write new checksums to file
         let mut file = File::create("files.rs").unwrap();
-        println!("f_str: {:?}",f_str);
         file.write_all(f_str.as_bytes()).unwrap();
     }
 }

+ 0 - 29
protocol/build.sh

@@ -1,29 +0,0 @@
-set -eu
-
-SRC="authentication keyexchange mercury
-     metadata pubsub spirc"
-
-cat > src/lib.rs <<EOF
-// Autogenerated by build.sh
-
-extern crate protobuf;
-EOF
-
-cat > files.rs <<EOF
-// Autogenerated by build.sh
-
-pub const FILES : &'static [(&'static str, u32)] = &[
-EOF
-
-for name in $SRC; do
-  src=proto/$name.proto
-  out=src/$name.rs
-  checksum=$(cksum $src | cut -f 1 -d' ')
-
-  protoc --rust_out src/ -I proto/ proto/$name.proto
-
-  echo "pub mod $name;" >> src/lib.rs
-  echo "    (\"$src\", $checksum)," >> files.rs
-done
-
-echo "];" >> files.rs

+ 77 - 78
protocol/src/authentication.rs

@@ -4965,84 +4965,83 @@ impl ::protobuf::reflect::ProtobufValue for AccountType {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x14authentication.proto\"\xec\x03\n\x17ClientResponseEncrypted\x12>\n\
-    \x11login_credentials\x18\n\x20\x02(\x0b2\x11.LoginCredentialsR\x10login\
-    Credentials\x12;\n\x10account_creation\x18\x14\x20\x01(\x0e2\x10.Account\
-    CreationR\x0faccountCreation\x12L\n\x14fingerprint_response\x18\x1e\x20\
-    \x01(\x0b2\x19.FingerprintResponseUnionR\x13fingerprintResponse\x121\n\
-    \x0bpeer_ticket\x18(\x20\x01(\x0b2\x10.PeerTicketUnionR\npeerTicket\x12,\
-    \n\x0bsystem_info\x182\x20\x02(\x0b2\x0b.SystemInfoR\nsystemInfo\x12%\n\
-    \x0eplatform_model\x18<\x20\x01(\tR\rplatformModel\x12%\n\x0eversion_str\
-    ing\x18F\x20\x01(\tR\rversionString\x12)\n\x06appkey\x18P\x20\x01(\x0b2\
-    \x11.LibspotifyAppKeyR\x06appkey\x12,\n\x0bclient_info\x18Z\x20\x01(\x0b\
-    2\x0b.ClientInfoR\nclientInfo\"r\n\x10LoginCredentials\x12\x1a\n\x08user\
-    name\x18\n\x20\x01(\tR\x08username\x12%\n\x03typ\x18\x14\x20\x02(\x0e2\
-    \x13.AuthenticationTypeR\x03typ\x12\x1b\n\tauth_data\x18\x1e\x20\x01(\
-    \x0cR\x08authData\"\x8c\x01\n\x18FingerprintResponseUnion\x12/\n\x05grai\
-    n\x18\n\x20\x01(\x0b2\x19.FingerprintGrainResponseR\x05grain\x12?\n\x0bh\
-    mac_ripemd\x18\x14\x20\x01(\x0b2\x1e.FingerprintHmacRipemdResponseR\nhma\
-    cRipemd\"?\n\x18FingerprintGrainResponse\x12#\n\rencrypted_key\x18\n\x20\
-    \x02(\x0cR\x0cencryptedKey\"3\n\x1dFingerprintHmacRipemdResponse\x12\x12\
-    \n\x04hmac\x18\n\x20\x02(\x0cR\x04hmac\"u\n\x0fPeerTicketUnion\x123\n\np\
-    ublic_key\x18\n\x20\x01(\x0b2\x14.PeerTicketPublicKeyR\tpublicKey\x12-\n\
-    \nold_ticket\x18\x14\x20\x01(\x0b2\x0e.PeerTicketOldR\toldTicket\"4\n\
-    \x13PeerTicketPublicKey\x12\x1d\n\npublic_key\x18\n\x20\x02(\x0cR\tpubli\
-    cKey\"d\n\rPeerTicketOld\x12\x1f\n\x0bpeer_ticket\x18\n\x20\x02(\x0cR\np\
-    eerTicket\x122\n\x15peer_ticket_signature\x18\x14\x20\x02(\x0cR\x13peerT\
-    icketSignature\"\xd4\x02\n\nSystemInfo\x12)\n\ncpu_family\x18\n\x20\x02(\
-    \x0e2\n.CpuFamilyR\tcpuFamily\x12\x1f\n\x0bcpu_subtype\x18\x14\x20\x01(\
-    \rR\ncpuSubtype\x12\x17\n\x07cpu_ext\x18\x1e\x20\x01(\rR\x06cpuExt\x12\
-    \x1c\n\x05brand\x18(\x20\x01(\x0e2\x06.BrandR\x05brand\x12\x1f\n\x0bbran\
-    d_flags\x182\x20\x01(\rR\nbrandFlags\x12\x13\n\x02os\x18<\x20\x02(\x0e2\
-    \x03.OsR\x02os\x12\x1d\n\nos_version\x18F\x20\x01(\rR\tosVersion\x12\x15\
-    \n\x06os_ext\x18P\x20\x01(\rR\x05osExt\x12:\n\x19system_information_stri\
-    ng\x18Z\x20\x01(\tR\x17systemInformationString\x12\x1b\n\tdevice_id\x18d\
-    \x20\x01(\tR\x08deviceId\"\xa5\x01\n\x10LibspotifyAppKey\x12\x18\n\x07ve\
-    rsion\x18\x01\x20\x02(\rR\x07version\x12\x16\n\x06devkey\x18\x02\x20\x02\
-    (\x0cR\x06devkey\x12\x1c\n\tsignature\x18\x03\x20\x02(\x0cR\tsignature\
-    \x12\x1c\n\tuseragent\x18\x04\x20\x02(\tR\tuseragent\x12#\n\rcallback_ha\
-    sh\x18\x05\x20\x02(\x0cR\x0ccallbackHash\"g\n\nClientInfo\x12\x18\n\x07l\
-    imited\x18\x01\x20\x01(\x08R\x07limited\x12#\n\x02fb\x18\x02\x20\x01(\
-    \x0b2\x13.ClientInfoFacebookR\x02fb\x12\x1a\n\x08language\x18\x03\x20\
-    \x01(\tR\x08language\"3\n\x12ClientInfoFacebook\x12\x1d\n\nmachine_id\
-    \x18\x01\x20\x01(\tR\tmachineId\"\xd4\x03\n\tAPWelcome\x12-\n\x12canonic\
-    al_username\x18\n\x20\x02(\tR\x11canonicalUsername\x12A\n\x16account_typ\
-    e_logged_in\x18\x14\x20\x02(\x0e2\x0c.AccountTypeR\x13accountTypeLoggedI\
-    n\x12I\n\x1acredentials_type_logged_in\x18\x19\x20\x02(\x0e2\x0c.Account\
-    TypeR\x17credentialsTypeLoggedIn\x12X\n\x1ereusable_auth_credentials_typ\
-    e\x18\x1e\x20\x02(\x0e2\x13.AuthenticationTypeR\x1breusableAuthCredentia\
-    lsType\x12:\n\x19reusable_auth_credentials\x18(\x20\x02(\x0cR\x17reusabl\
-    eAuthCredentials\x12\x1d\n\nlfs_secret\x182\x20\x01(\x0cR\tlfsSecret\x12\
-    /\n\x0caccount_info\x18<\x20\x01(\x0b2\x0c.AccountInfoR\x0baccountInfo\
-    \x12$\n\x02fb\x18F\x20\x01(\x0b2\x14.AccountInfoFacebookR\x02fb\"n\n\x0b\
-    AccountInfo\x12-\n\x07spotify\x18\x01\x20\x01(\x0b2\x13.AccountInfoSpoti\
-    fyR\x07spotify\x120\n\x08facebook\x18\x02\x20\x01(\x0b2\x14.AccountInfoF\
-    acebookR\x08facebook\"\x14\n\x12AccountInfoSpotify\"W\n\x13AccountInfoFa\
-    cebook\x12!\n\x0caccess_token\x18\x01\x20\x01(\tR\x0baccessToken\x12\x1d\
-    \n\nmachine_id\x18\x02\x20\x01(\tR\tmachineId*\xd6\x01\n\x12Authenticati\
-    onType\x12\x1c\n\x18AUTHENTICATION_USER_PASS\x10\0\x12-\n)AUTHENTICATION\
-    _STORED_SPOTIFY_CREDENTIALS\x10\x01\x12.\n*AUTHENTICATION_STORED_FACEBOO\
-    K_CREDENTIALS\x10\x02\x12\x20\n\x1cAUTHENTICATION_SPOTIFY_TOKEN\x10\x03\
-    \x12!\n\x1dAUTHENTICATION_FACEBOOK_TOKEN\x10\x04*Y\n\x0fAccountCreation\
-    \x12\"\n\x1eACCOUNT_CREATION_ALWAYS_PROMPT\x10\x01\x12\"\n\x1eACCOUNT_CR\
-    EATION_ALWAYS_CREATE\x10\x03*\x9d\x01\n\tCpuFamily\x12\x0f\n\x0bCPU_UNKN\
-    OWN\x10\0\x12\x0b\n\x07CPU_X86\x10\x01\x12\x0e\n\nCPU_X86_64\x10\x02\x12\
-    \x0b\n\x07CPU_PPC\x10\x03\x12\x0e\n\nCPU_PPC_64\x10\x04\x12\x0b\n\x07CPU\
-    _ARM\x10\x05\x12\x0c\n\x08CPU_IA64\x10\x06\x12\n\n\x06CPU_SH\x10\x07\x12\
-    \x0c\n\x08CPU_MIPS\x10\x08\x12\x10\n\x0cCPU_BLACKFIN\x10\t*K\n\x05Brand\
-    \x12\x13\n\x0fBRAND_UNBRANDED\x10\0\x12\r\n\tBRAND_INQ\x10\x01\x12\r\n\t\
-    BRAND_HTC\x10\x02\x12\x0f\n\x0bBRAND_NOKIA\x10\x03*\xd1\x02\n\x02Os\x12\
-    \x0e\n\nOS_UNKNOWN\x10\0\x12\x0e\n\nOS_WINDOWS\x10\x01\x12\n\n\x06OS_OSX\
-    \x10\x02\x12\r\n\tOS_IPHONE\x10\x03\x12\n\n\x06OS_S60\x10\x04\x12\x0c\n\
-    \x08OS_LINUX\x10\x05\x12\x11\n\rOS_WINDOWS_CE\x10\x06\x12\x0e\n\nOS_ANDR\
-    OID\x10\x07\x12\x0b\n\x07OS_PALM\x10\x08\x12\x0e\n\nOS_FREEBSD\x10\t\x12\
-    \x11\n\rOS_BLACKBERRY\x10\n\x12\x0c\n\x08OS_SONOS\x10\x0b\x12\x0f\n\x0bO\
-    S_LOGITECH\x10\x0c\x12\n\n\x06OS_WP7\x10\r\x12\x0c\n\x08OS_ONKYO\x10\x0e\
-    \x12\x0e\n\nOS_PHILIPS\x10\x0f\x12\t\n\x05OS_WD\x10\x10\x12\x0c\n\x08OS_\
-    VOLVO\x10\x11\x12\x0b\n\x07OS_TIVO\x10\x12\x12\x0b\n\x07OS_AWOX\x10\x13\
-    \x12\x0c\n\x08OS_MEEGO\x10\x14\x12\r\n\tOS_QNXNTO\x10\x15\x12\n\n\x06OS_\
-    BCO\x10\x16*(\n\x0bAccountType\x12\x0b\n\x07Spotify\x10\0\x12\x0c\n\x08F\
-    acebook\x10\x01\
+    \n\x14authentication.proto\x12\0\"\x8e\x03\n\x17ClientResponseEncrypted\
+    \x120\n\x11login_credentials\x18\n\x20\x02(\x0b2\x11.LoginCredentialsB\
+    \x02\x18\0\x12.\n\x10account_creation\x18\x14\x20\x01(\x0e2\x10.AccountC\
+    reationB\x02\x18\0\x12;\n\x14fingerprint_response\x18\x1e\x20\x01(\x0b2\
+    \x19.FingerprintResponseUnionB\x02\x18\0\x12)\n\x0bpeer_ticket\x18(\x20\
+    \x01(\x0b2\x10.PeerTicketUnionB\x02\x18\0\x12$\n\x0bsystem_info\x182\x20\
+    \x02(\x0b2\x0b.SystemInfoB\x02\x18\0\x12\x1a\n\x0eplatform_model\x18<\
+    \x20\x01(\tB\x02\x18\0\x12\x1a\n\x0eversion_string\x18F\x20\x01(\tB\x02\
+    \x18\0\x12%\n\x06appkey\x18P\x20\x01(\x0b2\x11.LibspotifyAppKeyB\x02\x18\
+    \0\x12$\n\x0bclient_info\x18Z\x20\x01(\x0b2\x0b.ClientInfoB\x02\x18\0\"e\
+    \n\x10LoginCredentials\x12\x14\n\x08username\x18\n\x20\x01(\tB\x02\x18\0\
+    \x12$\n\x03typ\x18\x14\x20\x02(\x0e2\x13.AuthenticationTypeB\x02\x18\0\
+    \x12\x15\n\tauth_data\x18\x1e\x20\x01(\x0cB\x02\x18\0\"\x81\x01\n\x18Fin\
+    gerprintResponseUnion\x12,\n\x05grain\x18\n\x20\x01(\x0b2\x19.Fingerprin\
+    tGrainResponseB\x02\x18\0\x127\n\x0bhmac_ripemd\x18\x14\x20\x01(\x0b2\
+    \x1e.FingerprintHmacRipemdResponseB\x02\x18\0\"5\n\x18FingerprintGrainRe\
+    sponse\x12\x19\n\rencrypted_key\x18\n\x20\x02(\x0cB\x02\x18\0\"1\n\x1dFi\
+    ngerprintHmacRipemdResponse\x12\x10\n\x04hmac\x18\n\x20\x02(\x0cB\x02\
+    \x18\0\"g\n\x0fPeerTicketUnion\x12,\n\npublic_key\x18\n\x20\x01(\x0b2\
+    \x14.PeerTicketPublicKeyB\x02\x18\0\x12&\n\nold_ticket\x18\x14\x20\x01(\
+    \x0b2\x0e.PeerTicketOldB\x02\x18\0\"-\n\x13PeerTicketPublicKey\x12\x16\n\
+    \npublic_key\x18\n\x20\x02(\x0cB\x02\x18\0\"K\n\rPeerTicketOld\x12\x17\n\
+    \x0bpeer_ticket\x18\n\x20\x02(\x0cB\x02\x18\0\x12!\n\x15peer_ticket_sign\
+    ature\x18\x14\x20\x02(\x0cB\x02\x18\0\"\x91\x02\n\nSystemInfo\x12\"\n\nc\
+    pu_family\x18\n\x20\x02(\x0e2\n.CpuFamilyB\x02\x18\0\x12\x17\n\x0bcpu_su\
+    btype\x18\x14\x20\x01(\rB\x02\x18\0\x12\x13\n\x07cpu_ext\x18\x1e\x20\x01\
+    (\rB\x02\x18\0\x12\x19\n\x05brand\x18(\x20\x01(\x0e2\x06.BrandB\x02\x18\
+    \0\x12\x17\n\x0bbrand_flags\x182\x20\x01(\rB\x02\x18\0\x12\x13\n\x02os\
+    \x18<\x20\x02(\x0e2\x03.OsB\x02\x18\0\x12\x16\n\nos_version\x18F\x20\x01\
+    (\rB\x02\x18\0\x12\x12\n\x06os_ext\x18P\x20\x01(\rB\x02\x18\0\x12%\n\x19\
+    system_information_string\x18Z\x20\x01(\tB\x02\x18\0\x12\x15\n\tdevice_i\
+    d\x18d\x20\x01(\tB\x02\x18\0\"\x84\x01\n\x10LibspotifyAppKey\x12\x13\n\
+    \x07version\x18\x01\x20\x02(\rB\x02\x18\0\x12\x12\n\x06devkey\x18\x02\
+    \x20\x02(\x0cB\x02\x18\0\x12\x15\n\tsignature\x18\x03\x20\x02(\x0cB\x02\
+    \x18\0\x12\x15\n\tuseragent\x18\x04\x20\x02(\tB\x02\x18\0\x12\x19\n\rcal\
+    lback_hash\x18\x05\x20\x02(\x0cB\x02\x18\0\"\\\n\nClientInfo\x12\x13\n\
+    \x07limited\x18\x01\x20\x01(\x08B\x02\x18\0\x12#\n\x02fb\x18\x02\x20\x01\
+    (\x0b2\x13.ClientInfoFacebookB\x02\x18\0\x12\x14\n\x08language\x18\x03\
+    \x20\x01(\tB\x02\x18\0\",\n\x12ClientInfoFacebook\x12\x16\n\nmachine_id\
+    \x18\x01\x20\x01(\tB\x02\x18\0\"\xe1\x02\n\tAPWelcome\x12\x1e\n\x12canon\
+    ical_username\x18\n\x20\x02(\tB\x02\x18\0\x120\n\x16account_type_logged_\
+    in\x18\x14\x20\x02(\x0e2\x0c.AccountTypeB\x02\x18\0\x124\n\x1acredential\
+    s_type_logged_in\x18\x19\x20\x02(\x0e2\x0c.AccountTypeB\x02\x18\0\x12?\n\
+    \x1ereusable_auth_credentials_type\x18\x1e\x20\x02(\x0e2\x13.Authenticat\
+    ionTypeB\x02\x18\0\x12%\n\x19reusable_auth_credentials\x18(\x20\x02(\x0c\
+    B\x02\x18\0\x12\x16\n\nlfs_secret\x182\x20\x01(\x0cB\x02\x18\0\x12&\n\
+    \x0caccount_info\x18<\x20\x01(\x0b2\x0c.AccountInfoB\x02\x18\0\x12$\n\
+    \x02fb\x18F\x20\x01(\x0b2\x14.AccountInfoFacebookB\x02\x18\0\"c\n\x0bAcc\
+    ountInfo\x12(\n\x07spotify\x18\x01\x20\x01(\x0b2\x13.AccountInfoSpotifyB\
+    \x02\x18\0\x12*\n\x08facebook\x18\x02\x20\x01(\x0b2\x14.AccountInfoFaceb\
+    ookB\x02\x18\0\"\x14\n\x12AccountInfoSpotify\"G\n\x13AccountInfoFacebook\
+    \x12\x18\n\x0caccess_token\x18\x01\x20\x01(\tB\x02\x18\0\x12\x16\n\nmach\
+    ine_id\x18\x02\x20\x01(\tB\x02\x18\0*\xda\x01\n\x12AuthenticationType\
+    \x12\x1c\n\x18AUTHENTICATION_USER_PASS\x10\0\x12-\n)AUTHENTICATION_STORE\
+    D_SPOTIFY_CREDENTIALS\x10\x01\x12.\n*AUTHENTICATION_STORED_FACEBOOK_CRED\
+    ENTIALS\x10\x02\x12\x20\n\x1cAUTHENTICATION_SPOTIFY_TOKEN\x10\x03\x12!\n\
+    \x1dAUTHENTICATION_FACEBOOK_TOKEN\x10\x04\x1a\x02\x10\0*]\n\x0fAccountCr\
+    eation\x12\"\n\x1eACCOUNT_CREATION_ALWAYS_PROMPT\x10\x01\x12\"\n\x1eACCO\
+    UNT_CREATION_ALWAYS_CREATE\x10\x03\x1a\x02\x10\0*\xa1\x01\n\tCpuFamily\
+    \x12\x0f\n\x0bCPU_UNKNOWN\x10\0\x12\x0b\n\x07CPU_X86\x10\x01\x12\x0e\n\n\
+    CPU_X86_64\x10\x02\x12\x0b\n\x07CPU_PPC\x10\x03\x12\x0e\n\nCPU_PPC_64\
+    \x10\x04\x12\x0b\n\x07CPU_ARM\x10\x05\x12\x0c\n\x08CPU_IA64\x10\x06\x12\
+    \n\n\x06CPU_SH\x10\x07\x12\x0c\n\x08CPU_MIPS\x10\x08\x12\x10\n\x0cCPU_BL\
+    ACKFIN\x10\t\x1a\x02\x10\0*O\n\x05Brand\x12\x13\n\x0fBRAND_UNBRANDED\x10\
+    \0\x12\r\n\tBRAND_INQ\x10\x01\x12\r\n\tBRAND_HTC\x10\x02\x12\x0f\n\x0bBR\
+    AND_NOKIA\x10\x03\x1a\x02\x10\0*\xd5\x02\n\x02Os\x12\x0e\n\nOS_UNKNOWN\
+    \x10\0\x12\x0e\n\nOS_WINDOWS\x10\x01\x12\n\n\x06OS_OSX\x10\x02\x12\r\n\t\
+    OS_IPHONE\x10\x03\x12\n\n\x06OS_S60\x10\x04\x12\x0c\n\x08OS_LINUX\x10\
+    \x05\x12\x11\n\rOS_WINDOWS_CE\x10\x06\x12\x0e\n\nOS_ANDROID\x10\x07\x12\
+    \x0b\n\x07OS_PALM\x10\x08\x12\x0e\n\nOS_FREEBSD\x10\t\x12\x11\n\rOS_BLAC\
+    KBERRY\x10\n\x12\x0c\n\x08OS_SONOS\x10\x0b\x12\x0f\n\x0bOS_LOGITECH\x10\
+    \x0c\x12\n\n\x06OS_WP7\x10\r\x12\x0c\n\x08OS_ONKYO\x10\x0e\x12\x0e\n\nOS\
+    _PHILIPS\x10\x0f\x12\t\n\x05OS_WD\x10\x10\x12\x0c\n\x08OS_VOLVO\x10\x11\
+    \x12\x0b\n\x07OS_TIVO\x10\x12\x12\x0b\n\x07OS_AWOX\x10\x13\x12\x0c\n\x08\
+    OS_MEEGO\x10\x14\x12\r\n\tOS_QNXNTO\x10\x15\x12\n\n\x06OS_BCO\x10\x16\
+    \x1a\x02\x10\0*,\n\x0bAccountType\x12\x0b\n\x07Spotify\x10\0\x12\x0c\n\
+    \x08Facebook\x10\x01\x1a\x02\x10\0B\0b\x06proto2\
 ";
 
 static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {

+ 97 - 99
protocol/src/keyexchange.rs

@@ -6705,105 +6705,103 @@ impl ::protobuf::reflect::ProtobufValue for ErrorCode {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x11keyexchange.proto\"\xb2\x03\n\x0bClientHello\x12)\n\nbuild_info\
-    \x18\n\x20\x02(\x0b2\n.BuildInfoR\tbuildInfo\x12C\n\x16fingerprints_supp\
-    orted\x18\x14\x20\x03(\x0e2\x0c.FingerprintR\x15fingerprintsSupported\
-    \x12C\n\x16cryptosuites_supported\x18\x1e\x20\x03(\x0e2\x0c.CryptosuiteR\
-    \x15cryptosuitesSupported\x12=\n\x14powschemes_supported\x18(\x20\x03(\
-    \x0e2\n.PowschemeR\x13powschemesSupported\x12D\n\x12login_crypto_hello\
-    \x182\x20\x02(\x0b2\x16.LoginCryptoHelloUnionR\x10loginCryptoHello\x12!\
-    \n\x0cclient_nonce\x18<\x20\x02(\x0cR\x0bclientNonce\x12\x18\n\x07paddin\
-    g\x18F\x20\x01(\x0cR\x07padding\x12,\n\x0bfeature_set\x18P\x20\x01(\x0b2\
-    \x0b.FeatureSetR\nfeatureSet\"\xa4\x01\n\tBuildInfo\x12\"\n\x07product\
-    \x18\n\x20\x02(\x0e2\x08.ProductR\x07product\x122\n\rproduct_flags\x18\
-    \x14\x20\x03(\x0e2\r.ProductFlagsR\x0cproductFlags\x12%\n\x08platform\
-    \x18\x1e\x20\x02(\x0e2\t.PlatformR\x08platform\x12\x18\n\x07version\x18(\
-    \x20\x02(\x04R\x07version\"^\n\x15LoginCryptoHelloUnion\x12E\n\x0ediffie\
-    _hellman\x18\n\x20\x01(\x0b2\x1e.LoginCryptoDiffieHellmanHelloR\rdiffieH\
-    ellman\"[\n\x1dLoginCryptoDiffieHellmanHello\x12\x0e\n\x02gc\x18\n\x20\
-    \x02(\x0cR\x02gc\x12*\n\x11server_keys_known\x18\x14\x20\x02(\rR\x0fserv\
-    erKeysKnown\"Y\n\nFeatureSet\x12\x20\n\x0bautoupdate2\x18\x01\x20\x01(\
-    \x08R\x0bautoupdate2\x12)\n\x10current_location\x18\x02\x20\x01(\x08R\
-    \x0fcurrentLocation\"\xa5\x01\n\x11APResponseMessage\x12*\n\tchallenge\
-    \x18\n\x20\x01(\x0b2\x0c.APChallengeR\tchallenge\x121\n\x07upgrade\x18\
-    \x14\x20\x01(\x0b2\x17.UpgradeRequiredMessageR\x07upgrade\x121\n\x0clogi\
-    n_failed\x18\x1e\x20\x01(\x0b2\x0e.APLoginFailedR\x0bloginFailed\"\xe8\
-    \x02\n\x0bAPChallenge\x12P\n\x16login_crypto_challenge\x18\n\x20\x02(\
-    \x0b2\x1a.LoginCryptoChallengeUnionR\x14loginCryptoChallenge\x12O\n\x15f\
-    ingerprint_challenge\x18\x14\x20\x02(\x0b2\x1a.FingerprintChallengeUnion\
-    R\x14fingerprintChallenge\x127\n\rpow_challenge\x18\x1e\x20\x02(\x0b2\
-    \x12.PoWChallengeUnionR\x0cpowChallenge\x12@\n\x10crypto_challenge\x18(\
-    \x20\x02(\x0b2\x15.CryptoChallengeUnionR\x0fcryptoChallenge\x12!\n\x0cse\
-    rver_nonce\x182\x20\x02(\x0cR\x0bserverNonce\x12\x18\n\x07padding\x18<\
-    \x20\x01(\x0cR\x07padding\"f\n\x19LoginCryptoChallengeUnion\x12I\n\x0edi\
-    ffie_hellman\x18\n\x20\x01(\x0b2\".LoginCryptoDiffieHellmanChallengeR\rd\
-    iffieHellman\"\x88\x01\n!LoginCryptoDiffieHellmanChallenge\x12\x0e\n\x02\
-    gs\x18\n\x20\x02(\x0cR\x02gs\x120\n\x14server_signature_key\x18\x14\x20\
-    \x02(\x05R\x12serverSignatureKey\x12!\n\x0cgs_signature\x18\x1e\x20\x02(\
-    \x0cR\x0bgsSignature\"\x8f\x01\n\x19FingerprintChallengeUnion\x120\n\x05\
-    grain\x18\n\x20\x01(\x0b2\x1a.FingerprintGrainChallengeR\x05grain\x12@\n\
-    \x0bhmac_ripemd\x18\x14\x20\x01(\x0b2\x1f.FingerprintHmacRipemdChallenge\
-    R\nhmacRipemd\"-\n\x19FingerprintGrainChallenge\x12\x10\n\x03kek\x18\n\
-    \x20\x02(\x0cR\x03kek\">\n\x1eFingerprintHmacRipemdChallenge\x12\x1c\n\t\
-    challenge\x18\n\x20\x02(\x0cR\tchallenge\"G\n\x11PoWChallengeUnion\x122\
-    \n\thash_cash\x18\n\x20\x01(\x0b2\x15.PoWHashCashChallengeR\x08hashCash\
-    \"^\n\x14PoWHashCashChallenge\x12\x16\n\x06prefix\x18\n\x20\x01(\x0cR\
-    \x06prefix\x12\x16\n\x06length\x18\x14\x20\x01(\x05R\x06length\x12\x16\n\
-    \x06target\x18\x1e\x20\x01(\x05R\x06target\"\x8a\x01\n\x14CryptoChalleng\
-    eUnion\x121\n\x07shannon\x18\n\x20\x01(\x0b2\x17.CryptoShannonChallengeR\
-    \x07shannon\x12?\n\rrc4_sha1_hmac\x18\x14\x20\x01(\x0b2\x1b.CryptoRc4Sha\
-    1HmacChallengeR\x0brc4Sha1Hmac\"\x18\n\x16CryptoShannonChallenge\"\x1c\n\
-    \x1aCryptoRc4Sha1HmacChallenge\"\x87\x01\n\x16UpgradeRequiredMessage\x12\
-    .\n\x13upgrade_signed_part\x18\n\x20\x02(\x0cR\x11upgradeSignedPart\x12\
-    \x1c\n\tsignature\x18\x14\x20\x02(\x0cR\tsignature\x12\x1f\n\x0bhttp_suf\
-    fix\x18\x1e\x20\x01(\tR\nhttpSuffix\"\xa0\x01\n\rAPLoginFailed\x12)\n\ne\
-    rror_code\x18\n\x20\x02(\x0e2\n.ErrorCodeR\terrorCode\x12\x1f\n\x0bretry\
-    _delay\x18\x14\x20\x01(\x05R\nretryDelay\x12\x16\n\x06expiry\x18\x1e\x20\
-    \x01(\x05R\x06expiry\x12+\n\x11error_description\x18(\x20\x01(\tR\x10err\
-    orDescription\"\xdd\x01\n\x17ClientResponsePlaintext\x12M\n\x15login_cry\
-    pto_response\x18\n\x20\x02(\x0b2\x19.LoginCryptoResponseUnionR\x13loginC\
-    ryptoResponse\x124\n\x0cpow_response\x18\x14\x20\x02(\x0b2\x11.PoWRespon\
-    seUnionR\x0bpowResponse\x12=\n\x0fcrypto_response\x18\x1e\x20\x02(\x0b2\
-    \x14.CryptoResponseUnionR\x0ecryptoResponse\"d\n\x18LoginCryptoResponseU\
-    nion\x12H\n\x0ediffie_hellman\x18\n\x20\x01(\x0b2!.LoginCryptoDiffieHell\
-    manResponseR\rdiffieHellman\"6\n\x20LoginCryptoDiffieHellmanResponse\x12\
-    \x12\n\x04hmac\x18\n\x20\x02(\x0cR\x04hmac\"E\n\x10PoWResponseUnion\x121\
-    \n\thash_cash\x18\n\x20\x01(\x0b2\x14.PoWHashCashResponseR\x08hashCash\"\
-    6\n\x13PoWHashCashResponse\x12\x1f\n\x0bhash_suffix\x18\n\x20\x02(\x0cR\
-    \nhashSuffix\"\x87\x01\n\x13CryptoResponseUnion\x120\n\x07shannon\x18\n\
-    \x20\x01(\x0b2\x16.CryptoShannonResponseR\x07shannon\x12>\n\rrc4_sha1_hm\
-    ac\x18\x14\x20\x01(\x0b2\x1a.CryptoRc4Sha1HmacResponseR\x0brc4Sha1Hmac\"\
-    -\n\x15CryptoShannonResponse\x12\x14\n\x05dummy\x18\x01\x20\x01(\x05R\
-    \x05dummy\"1\n\x19CryptoRc4Sha1HmacResponse\x12\x14\n\x05dummy\x18\x01\
-    \x20\x01(\x05R\x05dummy*\x7f\n\x07Product\x12\x12\n\x0ePRODUCT_CLIENT\
-    \x10\0\x12\x16\n\x12PRODUCT_LIBSPOTIFY\x10\x01\x12\x12\n\x0ePRODUCT_MOBI\
-    LE\x10\x02\x12\x13\n\x0fPRODUCT_PARTNER\x10\x03\x12\x1f\n\x1bPRODUCT_LIB\
-    SPOTIFY_EMBEDDED\x10\x05*A\n\x0cProductFlags\x12\x15\n\x11PRODUCT_FLAG_N\
-    ONE\x10\0\x12\x1a\n\x16PRODUCT_FLAG_DEV_BUILD\x10\x01*\xdc\x04\n\x08Plat\
-    form\x12\x16\n\x12PLATFORM_WIN32_X86\x10\0\x12\x14\n\x10PLATFORM_OSX_X86\
-    \x10\x01\x12\x16\n\x12PLATFORM_LINUX_X86\x10\x02\x12\x17\n\x13PLATFORM_I\
-    PHONE_ARM\x10\x03\x12\x14\n\x10PLATFORM_S60_ARM\x10\x04\x12\x14\n\x10PLA\
-    TFORM_OSX_PPC\x10\x05\x12\x18\n\x14PLATFORM_ANDROID_ARM\x10\x06\x12\x1b\
-    \n\x17PLATFORM_WINDOWS_CE_ARM\x10\x07\x12\x19\n\x15PLATFORM_LINUX_X86_64\
-    \x10\x08\x12\x17\n\x13PLATFORM_OSX_X86_64\x10\t\x12\x15\n\x11PLATFORM_PA\
-    LM_ARM\x10\n\x12\x15\n\x11PLATFORM_LINUX_SH\x10\x0b\x12\x18\n\x14PLATFOR\
-    M_FREEBSD_X86\x10\x0c\x12\x1b\n\x17PLATFORM_FREEBSD_X86_64\x10\r\x12\x1b\
-    \n\x17PLATFORM_BLACKBERRY_ARM\x10\x0e\x12\x12\n\x0ePLATFORM_SONOS\x10\
-    \x0f\x12\x17\n\x13PLATFORM_LINUX_MIPS\x10\x10\x12\x16\n\x12PLATFORM_LINU\
-    X_ARM\x10\x11\x12\x19\n\x15PLATFORM_LOGITECH_ARM\x10\x12\x12\x1b\n\x17PL\
-    ATFORM_LINUX_BLACKFIN\x10\x13\x12\x14\n\x10PLATFORM_WP7_ARM\x10\x14\x12\
-    \x16\n\x12PLATFORM_ONKYO_ARM\x10\x15\x12\x17\n\x13PLATFORM_QNXNTO_ARM\
-    \x10\x16\x12\x14\n\x10PLATFORM_BCO_ARM\x10\x17*A\n\x0bFingerprint\x12\
-    \x15\n\x11FINGERPRINT_GRAIN\x10\0\x12\x1b\n\x17FINGERPRINT_HMAC_RIPEMD\
-    \x10\x01*G\n\x0bCryptosuite\x12\x18\n\x14CRYPTO_SUITE_SHANNON\x10\0\x12\
-    \x1e\n\x1aCRYPTO_SUITE_RC4_SHA1_HMAC\x10\x01*\x1e\n\tPowscheme\x12\x11\n\
-    \rPOW_HASH_CASH\x10\0*\x89\x02\n\tErrorCode\x12\x11\n\rProtocolError\x10\
-    \0\x12\x10\n\x0cTryAnotherAP\x10\x02\x12\x13\n\x0fBadConnectionId\x10\
-    \x05\x12\x15\n\x11TravelRestriction\x10\t\x12\x1a\n\x16PremiumAccountReq\
-    uired\x10\x0b\x12\x12\n\x0eBadCredentials\x10\x0c\x12\x1f\n\x1bCouldNotV\
-    alidateCredentials\x10\r\x12\x11\n\rAccountExists\x10\x0e\x12\x1d\n\x19E\
-    xtraVerificationRequired\x10\x0f\x12\x11\n\rInvalidAppKey\x10\x10\x12\
-    \x15\n\x11ApplicationBanned\x10\x11\
+    \n\x11keyexchange.proto\x12\0\"\xd0\x02\n\x0bClientHello\x12\"\n\nbuild_\
+    info\x18\n\x20\x02(\x0b2\n.BuildInfoB\x02\x18\0\x120\n\x16fingerprints_s\
+    upported\x18\x14\x20\x03(\x0e2\x0c.FingerprintB\x02\x18\0\x120\n\x16cryp\
+    tosuites_supported\x18\x1e\x20\x03(\x0e2\x0c.CryptosuiteB\x02\x18\0\x12,\
+    \n\x14powschemes_supported\x18(\x20\x03(\x0e2\n.PowschemeB\x02\x18\0\x12\
+    6\n\x12login_crypto_hello\x182\x20\x02(\x0b2\x16.LoginCryptoHelloUnionB\
+    \x02\x18\0\x12\x18\n\x0cclient_nonce\x18<\x20\x02(\x0cB\x02\x18\0\x12\
+    \x13\n\x07padding\x18F\x20\x01(\x0cB\x02\x18\0\x12$\n\x0bfeature_set\x18\
+    P\x20\x01(\x0b2\x0b.FeatureSetB\x02\x18\0\"\x8a\x01\n\tBuildInfo\x12\x1d\
+    \n\x07product\x18\n\x20\x02(\x0e2\x08.ProductB\x02\x18\0\x12(\n\rproduct\
+    _flags\x18\x14\x20\x03(\x0e2\r.ProductFlagsB\x02\x18\0\x12\x1f\n\x08plat\
+    form\x18\x1e\x20\x02(\x0e2\t.PlatformB\x02\x18\0\x12\x13\n\x07version\
+    \x18(\x20\x02(\x04B\x02\x18\0\"S\n\x15LoginCryptoHelloUnion\x12:\n\x0edi\
+    ffie_hellman\x18\n\x20\x01(\x0b2\x1e.LoginCryptoDiffieHellmanHelloB\x02\
+    \x18\0\"N\n\x1dLoginCryptoDiffieHellmanHello\x12\x0e\n\x02gc\x18\n\x20\
+    \x02(\x0cB\x02\x18\0\x12\x1d\n\x11server_keys_known\x18\x14\x20\x02(\rB\
+    \x02\x18\0\"C\n\nFeatureSet\x12\x17\n\x0bautoupdate2\x18\x01\x20\x01(\
+    \x08B\x02\x18\0\x12\x1c\n\x10current_location\x18\x02\x20\x01(\x08B\x02\
+    \x18\0\"\x90\x01\n\x11APResponseMessage\x12#\n\tchallenge\x18\n\x20\x01(\
+    \x0b2\x0c.APChallengeB\x02\x18\0\x12,\n\x07upgrade\x18\x14\x20\x01(\x0b2\
+    \x17.UpgradeRequiredMessageB\x02\x18\0\x12(\n\x0clogin_failed\x18\x1e\
+    \x20\x01(\x0b2\x0e.APLoginFailedB\x02\x18\0\"\x9f\x02\n\x0bAPChallenge\
+    \x12>\n\x16login_crypto_challenge\x18\n\x20\x02(\x0b2\x1a.LoginCryptoCha\
+    llengeUnionB\x02\x18\0\x12=\n\x15fingerprint_challenge\x18\x14\x20\x02(\
+    \x0b2\x1a.FingerprintChallengeUnionB\x02\x18\0\x12-\n\rpow_challenge\x18\
+    \x1e\x20\x02(\x0b2\x12.PoWChallengeUnionB\x02\x18\0\x123\n\x10crypto_cha\
+    llenge\x18(\x20\x02(\x0b2\x15.CryptoChallengeUnionB\x02\x18\0\x12\x18\n\
+    \x0cserver_nonce\x182\x20\x02(\x0cB\x02\x18\0\x12\x13\n\x07padding\x18<\
+    \x20\x01(\x0cB\x02\x18\0\"[\n\x19LoginCryptoChallengeUnion\x12>\n\x0edif\
+    fie_hellman\x18\n\x20\x01(\x0b2\".LoginCryptoDiffieHellmanChallengeB\x02\
+    \x18\0\"o\n!LoginCryptoDiffieHellmanChallenge\x12\x0e\n\x02gs\x18\n\x20\
+    \x02(\x0cB\x02\x18\0\x12\x20\n\x14server_signature_key\x18\x14\x20\x02(\
+    \x05B\x02\x18\0\x12\x18\n\x0cgs_signature\x18\x1e\x20\x02(\x0cB\x02\x18\
+    \0\"\x84\x01\n\x19FingerprintChallengeUnion\x12-\n\x05grain\x18\n\x20\
+    \x01(\x0b2\x1a.FingerprintGrainChallengeB\x02\x18\0\x128\n\x0bhmac_ripem\
+    d\x18\x14\x20\x01(\x0b2\x1f.FingerprintHmacRipemdChallengeB\x02\x18\0\",\
+    \n\x19FingerprintGrainChallenge\x12\x0f\n\x03kek\x18\n\x20\x02(\x0cB\x02\
+    \x18\0\"7\n\x1eFingerprintHmacRipemdChallenge\x12\x15\n\tchallenge\x18\n\
+    \x20\x02(\x0cB\x02\x18\0\"A\n\x11PoWChallengeUnion\x12,\n\thash_cash\x18\
+    \n\x20\x01(\x0b2\x15.PoWHashCashChallengeB\x02\x18\0\"R\n\x14PoWHashCash\
+    Challenge\x12\x12\n\x06prefix\x18\n\x20\x01(\x0cB\x02\x18\0\x12\x12\n\
+    \x06length\x18\x14\x20\x01(\x05B\x02\x18\0\x12\x12\n\x06target\x18\x1e\
+    \x20\x01(\x05B\x02\x18\0\"|\n\x14CryptoChallengeUnion\x12,\n\x07shannon\
+    \x18\n\x20\x01(\x0b2\x17.CryptoShannonChallengeB\x02\x18\0\x126\n\rrc4_s\
+    ha1_hmac\x18\x14\x20\x01(\x0b2\x1b.CryptoRc4Sha1HmacChallengeB\x02\x18\0\
+    \"\x18\n\x16CryptoShannonChallenge\"\x1c\n\x1aCryptoRc4Sha1HmacChallenge\
+    \"i\n\x16UpgradeRequiredMessage\x12\x1f\n\x13upgrade_signed_part\x18\n\
+    \x20\x02(\x0cB\x02\x18\0\x12\x15\n\tsignature\x18\x14\x20\x02(\x0cB\x02\
+    \x18\0\x12\x17\n\x0bhttp_suffix\x18\x1e\x20\x01(\tB\x02\x18\0\"\x7f\n\rA\
+    PLoginFailed\x12\"\n\nerror_code\x18\n\x20\x02(\x0e2\n.ErrorCodeB\x02\
+    \x18\0\x12\x17\n\x0bretry_delay\x18\x14\x20\x01(\x05B\x02\x18\0\x12\x12\
+    \n\x06expiry\x18\x1e\x20\x01(\x05B\x02\x18\0\x12\x1d\n\x11error_descript\
+    ion\x18(\x20\x01(\tB\x02\x18\0\"\xb7\x01\n\x17ClientResponsePlaintext\
+    \x12<\n\x15login_crypto_response\x18\n\x20\x02(\x0b2\x19.LoginCryptoResp\
+    onseUnionB\x02\x18\0\x12+\n\x0cpow_response\x18\x14\x20\x02(\x0b2\x11.Po\
+    WResponseUnionB\x02\x18\0\x121\n\x0fcrypto_response\x18\x1e\x20\x02(\x0b\
+    2\x14.CryptoResponseUnionB\x02\x18\0\"Y\n\x18LoginCryptoResponseUnion\
+    \x12=\n\x0ediffie_hellman\x18\n\x20\x01(\x0b2!.LoginCryptoDiffieHellmanR\
+    esponseB\x02\x18\0\"4\n\x20LoginCryptoDiffieHellmanResponse\x12\x10\n\
+    \x04hmac\x18\n\x20\x02(\x0cB\x02\x18\0\"?\n\x10PoWResponseUnion\x12+\n\t\
+    hash_cash\x18\n\x20\x01(\x0b2\x14.PoWHashCashResponseB\x02\x18\0\".\n\
+    \x13PoWHashCashResponse\x12\x17\n\x0bhash_suffix\x18\n\x20\x02(\x0cB\x02\
+    \x18\0\"y\n\x13CryptoResponseUnion\x12+\n\x07shannon\x18\n\x20\x01(\x0b2\
+    \x16.CryptoShannonResponseB\x02\x18\0\x125\n\rrc4_sha1_hmac\x18\x14\x20\
+    \x01(\x0b2\x1a.CryptoRc4Sha1HmacResponseB\x02\x18\0\"*\n\x15CryptoShanno\
+    nResponse\x12\x11\n\x05dummy\x18\x01\x20\x01(\x05B\x02\x18\0\".\n\x19Cry\
+    ptoRc4Sha1HmacResponse\x12\x11\n\x05dummy\x18\x01\x20\x01(\x05B\x02\x18\
+    \0*\x83\x01\n\x07Product\x12\x12\n\x0ePRODUCT_CLIENT\x10\0\x12\x16\n\x12\
+    PRODUCT_LIBSPOTIFY\x10\x01\x12\x12\n\x0ePRODUCT_MOBILE\x10\x02\x12\x13\n\
+    \x0fPRODUCT_PARTNER\x10\x03\x12\x1f\n\x1bPRODUCT_LIBSPOTIFY_EMBEDDED\x10\
+    \x05\x1a\x02\x10\0*E\n\x0cProductFlags\x12\x15\n\x11PRODUCT_FLAG_NONE\
+    \x10\0\x12\x1a\n\x16PRODUCT_FLAG_DEV_BUILD\x10\x01\x1a\x02\x10\0*\xe0\
+    \x04\n\x08Platform\x12\x16\n\x12PLATFORM_WIN32_X86\x10\0\x12\x14\n\x10PL\
+    ATFORM_OSX_X86\x10\x01\x12\x16\n\x12PLATFORM_LINUX_X86\x10\x02\x12\x17\n\
+    \x13PLATFORM_IPHONE_ARM\x10\x03\x12\x14\n\x10PLATFORM_S60_ARM\x10\x04\
+    \x12\x14\n\x10PLATFORM_OSX_PPC\x10\x05\x12\x18\n\x14PLATFORM_ANDROID_ARM\
+    \x10\x06\x12\x1b\n\x17PLATFORM_WINDOWS_CE_ARM\x10\x07\x12\x19\n\x15PLATF\
+    ORM_LINUX_X86_64\x10\x08\x12\x17\n\x13PLATFORM_OSX_X86_64\x10\t\x12\x15\
+    \n\x11PLATFORM_PALM_ARM\x10\n\x12\x15\n\x11PLATFORM_LINUX_SH\x10\x0b\x12\
+    \x18\n\x14PLATFORM_FREEBSD_X86\x10\x0c\x12\x1b\n\x17PLATFORM_FREEBSD_X86\
+    _64\x10\r\x12\x1b\n\x17PLATFORM_BLACKBERRY_ARM\x10\x0e\x12\x12\n\x0ePLAT\
+    FORM_SONOS\x10\x0f\x12\x17\n\x13PLATFORM_LINUX_MIPS\x10\x10\x12\x16\n\
+    \x12PLATFORM_LINUX_ARM\x10\x11\x12\x19\n\x15PLATFORM_LOGITECH_ARM\x10\
+    \x12\x12\x1b\n\x17PLATFORM_LINUX_BLACKFIN\x10\x13\x12\x14\n\x10PLATFORM_\
+    WP7_ARM\x10\x14\x12\x16\n\x12PLATFORM_ONKYO_ARM\x10\x15\x12\x17\n\x13PLA\
+    TFORM_QNXNTO_ARM\x10\x16\x12\x14\n\x10PLATFORM_BCO_ARM\x10\x17\x1a\x02\
+    \x10\0*E\n\x0bFingerprint\x12\x15\n\x11FINGERPRINT_GRAIN\x10\0\x12\x1b\n\
+    \x17FINGERPRINT_HMAC_RIPEMD\x10\x01\x1a\x02\x10\0*K\n\x0bCryptosuite\x12\
+    \x18\n\x14CRYPTO_SUITE_SHANNON\x10\0\x12\x1e\n\x1aCRYPTO_SUITE_RC4_SHA1_\
+    HMAC\x10\x01\x1a\x02\x10\0*\"\n\tPowscheme\x12\x11\n\rPOW_HASH_CASH\x10\
+    \0\x1a\x02\x10\0*\x8d\x02\n\tErrorCode\x12\x11\n\rProtocolError\x10\0\
+    \x12\x10\n\x0cTryAnotherAP\x10\x02\x12\x13\n\x0fBadConnectionId\x10\x05\
+    \x12\x15\n\x11TravelRestriction\x10\t\x12\x1a\n\x16PremiumAccountRequire\
+    d\x10\x0b\x12\x12\n\x0eBadCredentials\x10\x0c\x12\x1f\n\x1bCouldNotValid\
+    ateCredentials\x10\r\x12\x11\n\rAccountExists\x10\x0e\x12\x1d\n\x19Extra\
+    VerificationRequired\x10\x0f\x12\x11\n\rInvalidAppKey\x10\x10\x12\x15\n\
+    \x11ApplicationBanned\x10\x11\x1a\x02\x10\0B\0b\x06proto2\
 ";
 
 static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {

+ 21 - 20
protocol/src/mercury.rs

@@ -1775,26 +1775,27 @@ impl ::protobuf::reflect::ProtobufValue for UserField {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\rmercury.proto\"C\n\x16MercuryMultiGetRequest\x12)\n\x07request\x18\
-    \x01\x20\x03(\x0b2\x0f.MercuryRequestR\x07request\";\n\x14MercuryMultiGe\
-    tReply\x12#\n\x05reply\x18\x01\x20\x03(\x0b2\r.MercuryReplyR\x05reply\"m\
-    \n\x0eMercuryRequest\x12\x10\n\x03uri\x18\x01\x20\x01(\tR\x03uri\x12!\n\
-    \x0ccontent_type\x18\x02\x20\x01(\tR\x0bcontentType\x12\x12\n\x04body\
-    \x18\x03\x20\x01(\x0cR\x04body\x12\x12\n\x04etag\x18\x04\x20\x01(\x0cR\
-    \x04etag\"\xb3\x02\n\x0cMercuryReply\x12\x1f\n\x0bstatus_code\x18\x01\
-    \x20\x01(\x11R\nstatusCode\x12%\n\x0estatus_message\x18\x02\x20\x01(\tR\
-    \rstatusMessage\x12<\n\x0ccache_policy\x18\x03\x20\x01(\x0e2\x19.Mercury\
-    Reply.CachePolicyR\x0bcachePolicy\x12\x10\n\x03ttl\x18\x04\x20\x01(\x11R\
-    \x03ttl\x12\x12\n\x04etag\x18\x05\x20\x01(\x0cR\x04etag\x12!\n\x0cconten\
-    t_type\x18\x06\x20\x01(\tR\x0bcontentType\x12\x12\n\x04body\x18\x07\x20\
-    \x01(\x0cR\x04body\"@\n\x0bCachePolicy\x12\x0c\n\x08CACHE_NO\x10\x01\x12\
-    \x11\n\rCACHE_PRIVATE\x10\x02\x12\x10\n\x0cCACHE_PUBLIC\x10\x03\"\xa3\
-    \x01\n\x06Header\x12\x10\n\x03uri\x18\x01\x20\x01(\tR\x03uri\x12!\n\x0cc\
-    ontent_type\x18\x02\x20\x01(\tR\x0bcontentType\x12\x16\n\x06method\x18\
-    \x03\x20\x01(\tR\x06method\x12\x1f\n\x0bstatus_code\x18\x04\x20\x01(\x11\
-    R\nstatusCode\x12+\n\x0buser_fields\x18\x06\x20\x03(\x0b2\n.UserFieldR\n\
-    userFields\"3\n\tUserField\x12\x10\n\x03key\x18\x01\x20\x01(\tR\x03key\
-    \x12\x14\n\x05value\x18\x02\x20\x01(\x0cR\x05value\
+    \n\rmercury.proto\x12\0\">\n\x16MercuryMultiGetRequest\x12$\n\x07request\
+    \x18\x01\x20\x03(\x0b2\x0f.MercuryRequestB\x02\x18\0\"8\n\x14MercuryMult\
+    iGetReply\x12\x20\n\x05reply\x18\x01\x20\x03(\x0b2\r.MercuryReplyB\x02\
+    \x18\0\"_\n\x0eMercuryRequest\x12\x0f\n\x03uri\x18\x01\x20\x01(\tB\x02\
+    \x18\0\x12\x18\n\x0ccontent_type\x18\x02\x20\x01(\tB\x02\x18\0\x12\x10\n\
+    \x04body\x18\x03\x20\x01(\x0cB\x02\x18\0\x12\x10\n\x04etag\x18\x04\x20\
+    \x01(\x0cB\x02\x18\0\"\x8d\x02\n\x0cMercuryReply\x12\x17\n\x0bstatus_cod\
+    e\x18\x01\x20\x01(\x11B\x02\x18\0\x12\x1a\n\x0estatus_message\x18\x02\
+    \x20\x01(\tB\x02\x18\0\x123\n\x0ccache_policy\x18\x03\x20\x01(\x0e2\x19.\
+    MercuryReply.CachePolicyB\x02\x18\0\x12\x0f\n\x03ttl\x18\x04\x20\x01(\
+    \x11B\x02\x18\0\x12\x10\n\x04etag\x18\x05\x20\x01(\x0cB\x02\x18\0\x12\
+    \x18\n\x0ccontent_type\x18\x06\x20\x01(\tB\x02\x18\0\x12\x10\n\x04body\
+    \x18\x07\x20\x01(\x0cB\x02\x18\0\"D\n\x0bCachePolicy\x12\x0c\n\x08CACHE_\
+    NO\x10\x01\x12\x11\n\rCACHE_PRIVATE\x10\x02\x12\x10\n\x0cCACHE_PUBLIC\
+    \x10\x03\x1a\x02\x10\0\"\x85\x01\n\x06Header\x12\x0f\n\x03uri\x18\x01\
+    \x20\x01(\tB\x02\x18\0\x12\x18\n\x0ccontent_type\x18\x02\x20\x01(\tB\x02\
+    \x18\0\x12\x12\n\x06method\x18\x03\x20\x01(\tB\x02\x18\0\x12\x17\n\x0bst\
+    atus_code\x18\x04\x20\x01(\x11B\x02\x18\0\x12#\n\x0buser_fields\x18\x06\
+    \x20\x03(\x0b2\n.UserFieldB\x02\x18\0\"/\n\tUserField\x12\x0f\n\x03key\
+    \x18\x01\x20\x01(\tB\x02\x18\0\x12\x11\n\x05value\x18\x02\x20\x01(\x0cB\
+    \x02\x18\0B\0b\x06proto2\
 ";
 
 static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {

+ 86 - 85
protocol/src/metadata.rs

@@ -6093,91 +6093,92 @@ impl ::protobuf::reflect::ProtobufValue for AudioFile_Format {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0emetadata.proto\"C\n\tTopTracks\x12\x18\n\x07country\x18\x01\x20\
-    \x01(\tR\x07country\x12\x1c\n\x05track\x18\x02\x20\x03(\x0b2\x06.TrackR\
-    \x05track\"b\n\x0eActivityPeriod\x12\x1d\n\nstart_year\x18\x01\x20\x01(\
-    \x11R\tstartYear\x12\x19\n\x08end_year\x18\x02\x20\x01(\x11R\x07endYear\
-    \x12\x16\n\x06decade\x18\x03\x20\x01(\x11R\x06decade\"\xd0\x05\n\x06Arti\
-    st\x12\x10\n\x03gid\x18\x01\x20\x01(\x0cR\x03gid\x12\x12\n\x04name\x18\
-    \x02\x20\x01(\tR\x04name\x12\x1e\n\npopularity\x18\x03\x20\x01(\x11R\npo\
-    pularity\x12'\n\ttop_track\x18\x04\x20\x03(\x0b2\n.TopTracksR\x08topTrac\
-    k\x12,\n\x0balbum_group\x18\x05\x20\x03(\x0b2\x0b.AlbumGroupR\nalbumGrou\
-    p\x12.\n\x0csingle_group\x18\x06\x20\x03(\x0b2\x0b.AlbumGroupR\x0bsingle\
-    Group\x128\n\x11compilation_group\x18\x07\x20\x03(\x0b2\x0b.AlbumGroupR\
-    \x10compilationGroup\x125\n\x10appears_on_group\x18\x08\x20\x03(\x0b2\
-    \x0b.AlbumGroupR\x0eappearsOnGroup\x12\x14\n\x05genre\x18\t\x20\x03(\tR\
-    \x05genre\x12,\n\x0bexternal_id\x18\n\x20\x03(\x0b2\x0b.ExternalIdR\next\
-    ernalId\x12\"\n\x08portrait\x18\x0b\x20\x03(\x0b2\x06.ImageR\x08portrait\
-    \x12(\n\tbiography\x18\x0c\x20\x03(\x0b2\n.BiographyR\tbiography\x128\n\
-    \x0factivity_period\x18\r\x20\x03(\x0b2\x0f.ActivityPeriodR\x0eactivityP\
-    eriod\x12.\n\x0brestriction\x18\x0e\x20\x03(\x0b2\x0c.RestrictionR\x0bre\
-    striction\x12!\n\x07related\x18\x0f\x20\x03(\x0b2\x07.ArtistR\x07related\
-    \x125\n\x17is_portrait_album_cover\x18\x10\x20\x01(\x08R\x14isPortraitAl\
-    bumCover\x122\n\x0eportrait_group\x18\x11\x20\x01(\x0b2\x0b.ImageGroupR\
-    \rportraitGroup\"*\n\nAlbumGroup\x12\x1c\n\x05album\x18\x01\x20\x03(\x0b\
-    2\x06.AlbumR\x05album\"B\n\x04Date\x12\x12\n\x04year\x18\x01\x20\x01(\
-    \x11R\x04year\x12\x14\n\x05month\x18\x02\x20\x01(\x11R\x05month\x12\x10\
-    \n\x03day\x18\x03\x20\x01(\x11R\x03day\"\xe3\x04\n\x05Album\x12\x10\n\
-    \x03gid\x18\x01\x20\x01(\x0cR\x03gid\x12\x12\n\x04name\x18\x02\x20\x01(\
-    \tR\x04name\x12\x1f\n\x06artist\x18\x03\x20\x03(\x0b2\x07.ArtistR\x06art\
-    ist\x12\x1d\n\x03typ\x18\x04\x20\x01(\x0e2\x0b.Album.TypeR\x03typ\x12\
-    \x14\n\x05label\x18\x05\x20\x01(\tR\x05label\x12\x19\n\x04date\x18\x06\
-    \x20\x01(\x0b2\x05.DateR\x04date\x12\x1e\n\npopularity\x18\x07\x20\x01(\
-    \x11R\npopularity\x12\x14\n\x05genre\x18\x08\x20\x03(\tR\x05genre\x12\
-    \x1c\n\x05cover\x18\t\x20\x03(\x0b2\x06.ImageR\x05cover\x12,\n\x0bextern\
-    al_id\x18\n\x20\x03(\x0b2\x0b.ExternalIdR\nexternalId\x12\x19\n\x04disc\
-    \x18\x0b\x20\x03(\x0b2\x05.DiscR\x04disc\x12\x16\n\x06review\x18\x0c\x20\
-    \x03(\tR\x06review\x12(\n\tcopyright\x18\r\x20\x03(\x0b2\n.CopyrightR\tc\
-    opyright\x12.\n\x0brestriction\x18\x0e\x20\x03(\x0b2\x0c.RestrictionR\
-    \x0brestriction\x12\x20\n\x07related\x18\x0f\x20\x03(\x0b2\x06.AlbumR\
-    \x07related\x12,\n\x0bsale_period\x18\x10\x20\x03(\x0b2\x0b.SalePeriodR\
-    \nsalePeriod\x12,\n\x0bcover_group\x18\x11\x20\x01(\x0b2\x0b.ImageGroupR\
-    \ncoverGroup\"6\n\x04Type\x12\t\n\x05ALBUM\x10\x01\x12\n\n\x06SINGLE\x10\
-    \x02\x12\x0f\n\x0bCOMPILATION\x10\x03\x12\x06\n\x02EP\x10\x04\"\xf9\x03\
-    \n\x05Track\x12\x10\n\x03gid\x18\x01\x20\x01(\x0cR\x03gid\x12\x12\n\x04n\
-    ame\x18\x02\x20\x01(\tR\x04name\x12\x1c\n\x05album\x18\x03\x20\x01(\x0b2\
-    \x06.AlbumR\x05album\x12\x1f\n\x06artist\x18\x04\x20\x03(\x0b2\x07.Artis\
-    tR\x06artist\x12\x16\n\x06number\x18\x05\x20\x01(\x11R\x06number\x12\x1f\
-    \n\x0bdisc_number\x18\x06\x20\x01(\x11R\ndiscNumber\x12\x1a\n\x08duratio\
-    n\x18\x07\x20\x01(\x11R\x08duration\x12\x1e\n\npopularity\x18\x08\x20\
-    \x01(\x11R\npopularity\x12\x1a\n\x08explicit\x18\t\x20\x01(\x08R\x08expl\
-    icit\x12,\n\x0bexternal_id\x18\n\x20\x03(\x0b2\x0b.ExternalIdR\nexternal\
-    Id\x12.\n\x0brestriction\x18\x0b\x20\x03(\x0b2\x0c.RestrictionR\x0brestr\
-    iction\x12\x1e\n\x04file\x18\x0c\x20\x03(\x0b2\n.AudioFileR\x04file\x12(\
-    \n\x0balternative\x18\r\x20\x03(\x0b2\x06.TrackR\x0balternative\x12,\n\
-    \x0bsale_period\x18\x0e\x20\x03(\x0b2\x0b.SalePeriodR\nsalePeriod\x12$\n\
-    \x07preview\x18\x0f\x20\x03(\x0b2\n.AudioFileR\x07preview\"\xa6\x01\n\
-    \x05Image\x12\x17\n\x07file_id\x18\x01\x20\x01(\x0cR\x06fileId\x12\x1f\n\
-    \x04size\x18\x02\x20\x01(\x0e2\x0b.Image.SizeR\x04size\x12\x14\n\x05widt\
-    h\x18\x03\x20\x01(\x11R\x05width\x12\x16\n\x06height\x18\x04\x20\x01(\
-    \x11R\x06height\"5\n\x04Size\x12\x0b\n\x07DEFAULT\x10\0\x12\t\n\x05SMALL\
-    \x10\x01\x12\t\n\x05LARGE\x10\x02\x12\n\n\x06XLARGE\x10\x03\"*\n\nImageG\
-    roup\x12\x1c\n\x05image\x18\x01\x20\x03(\x0b2\x06.ImageR\x05image\"w\n\t\
-    Biography\x12\x12\n\x04text\x18\x01\x20\x01(\tR\x04text\x12\"\n\x08portr\
-    ait\x18\x02\x20\x03(\x0b2\x06.ImageR\x08portrait\x122\n\x0eportrait_grou\
-    p\x18\x03\x20\x03(\x0b2\x0b.ImageGroupR\rportraitGroup\"P\n\x04Disc\x12\
-    \x16\n\x06number\x18\x01\x20\x01(\x11R\x06number\x12\x12\n\x04name\x18\
-    \x02\x20\x01(\tR\x04name\x12\x1c\n\x05track\x18\x03\x20\x03(\x0b2\x06.Tr\
-    ackR\x05track\"X\n\tCopyright\x12!\n\x03typ\x18\x01\x20\x01(\x0e2\x0f.Co\
-    pyright.TypeR\x03typ\x12\x12\n\x04text\x18\x02\x20\x01(\tR\x04text\"\x14\
-    \n\x04Type\x12\x05\n\x01P\x10\0\x12\x05\n\x01C\x10\x01\"\xcc\x01\n\x0bRe\
-    striction\x12+\n\x11countries_allowed\x18\x02\x20\x01(\tR\x10countriesAl\
-    lowed\x12/\n\x13countries_forbidden\x18\x03\x20\x01(\tR\x12countriesForb\
-    idden\x12#\n\x03typ\x18\x04\x20\x01(\x0e2\x11.Restriction.TypeR\x03typ\
-    \x12#\n\rcatalogue_str\x18\x05\x20\x03(\tR\x0ccatalogueStr\"\x15\n\x04Ty\
-    pe\x12\r\n\tSTREAMING\x10\0\"r\n\nSalePeriod\x12.\n\x0brestriction\x18\
-    \x01\x20\x03(\x0b2\x0c.RestrictionR\x0brestriction\x12\x1b\n\x05start\
-    \x18\x02\x20\x01(\x0b2\x05.DateR\x05start\x12\x17\n\x03end\x18\x03\x20\
-    \x01(\x0b2\x05.DateR\x03end\".\n\nExternalId\x12\x10\n\x03typ\x18\x01\
-    \x20\x01(\tR\x03typ\x12\x0e\n\x02id\x18\x02\x20\x01(\tR\x02id\"\xa3\x02\
-    \n\tAudioFile\x12\x17\n\x07file_id\x18\x01\x20\x01(\x0cR\x06fileId\x12)\
-    \n\x06format\x18\x02\x20\x01(\x0e2\x11.AudioFile.FormatR\x06format\"\xd1\
-    \x01\n\x06Format\x12\x11\n\rOGG_VORBIS_96\x10\0\x12\x12\n\x0eOGG_VORBIS_\
-    160\x10\x01\x12\x12\n\x0eOGG_VORBIS_320\x10\x02\x12\x0b\n\x07MP3_256\x10\
-    \x03\x12\x0b\n\x07MP3_320\x10\x04\x12\x0b\n\x07MP3_160\x10\x05\x12\n\n\
-    \x06MP3_96\x10\x06\x12\x0f\n\x0bMP3_160_ENC\x10\x07\x12\n\n\x06OTHER2\
-    \x10\x08\x12\n\n\x06OTHER3\x10\t\x12\x0b\n\x07AAC_160\x10\n\x12\x0b\n\
-    \x07AAC_320\x10\x0b\x12\n\n\x06OTHER4\x10\x0c\x12\n\n\x06OTHER5\x10\r\
+    \n\x0emetadata.proto\x12\0\";\n\tTopTracks\x12\x13\n\x07country\x18\x01\
+    \x20\x01(\tB\x02\x18\0\x12\x19\n\x05track\x18\x02\x20\x03(\x0b2\x06.Trac\
+    kB\x02\x18\0\"R\n\x0eActivityPeriod\x12\x16\n\nstart_year\x18\x01\x20\
+    \x01(\x11B\x02\x18\0\x12\x14\n\x08end_year\x18\x02\x20\x01(\x11B\x02\x18\
+    \0\x12\x12\n\x06decade\x18\x03\x20\x01(\x11B\x02\x18\0\"\xc5\x04\n\x06Ar\
+    tist\x12\x0f\n\x03gid\x18\x01\x20\x01(\x0cB\x02\x18\0\x12\x10\n\x04name\
+    \x18\x02\x20\x01(\tB\x02\x18\0\x12\x16\n\npopularity\x18\x03\x20\x01(\
+    \x11B\x02\x18\0\x12!\n\ttop_track\x18\x04\x20\x03(\x0b2\n.TopTracksB\x02\
+    \x18\0\x12$\n\x0balbum_group\x18\x05\x20\x03(\x0b2\x0b.AlbumGroupB\x02\
+    \x18\0\x12%\n\x0csingle_group\x18\x06\x20\x03(\x0b2\x0b.AlbumGroupB\x02\
+    \x18\0\x12*\n\x11compilation_group\x18\x07\x20\x03(\x0b2\x0b.AlbumGroupB\
+    \x02\x18\0\x12)\n\x10appears_on_group\x18\x08\x20\x03(\x0b2\x0b.AlbumGro\
+    upB\x02\x18\0\x12\x11\n\x05genre\x18\t\x20\x03(\tB\x02\x18\0\x12$\n\x0be\
+    xternal_id\x18\n\x20\x03(\x0b2\x0b.ExternalIdB\x02\x18\0\x12\x1c\n\x08po\
+    rtrait\x18\x0b\x20\x03(\x0b2\x06.ImageB\x02\x18\0\x12!\n\tbiography\x18\
+    \x0c\x20\x03(\x0b2\n.BiographyB\x02\x18\0\x12,\n\x0factivity_period\x18\
+    \r\x20\x03(\x0b2\x0f.ActivityPeriodB\x02\x18\0\x12%\n\x0brestriction\x18\
+    \x0e\x20\x03(\x0b2\x0c.RestrictionB\x02\x18\0\x12\x1c\n\x07related\x18\
+    \x0f\x20\x03(\x0b2\x07.ArtistB\x02\x18\0\x12#\n\x17is_portrait_album_cov\
+    er\x18\x10\x20\x01(\x08B\x02\x18\0\x12'\n\x0eportrait_group\x18\x11\x20\
+    \x01(\x0b2\x0b.ImageGroupB\x02\x18\0\"'\n\nAlbumGroup\x12\x19\n\x05album\
+    \x18\x01\x20\x03(\x0b2\x06.AlbumB\x02\x18\0\"<\n\x04Date\x12\x10\n\x04ye\
+    ar\x18\x01\x20\x01(\x11B\x02\x18\0\x12\x11\n\x05month\x18\x02\x20\x01(\
+    \x11B\x02\x18\0\x12\x0f\n\x03day\x18\x03\x20\x01(\x11B\x02\x18\0\"\x99\
+    \x04\n\x05Album\x12\x0f\n\x03gid\x18\x01\x20\x01(\x0cB\x02\x18\0\x12\x10\
+    \n\x04name\x18\x02\x20\x01(\tB\x02\x18\0\x12\x1b\n\x06artist\x18\x03\x20\
+    \x03(\x0b2\x07.ArtistB\x02\x18\0\x12\x1c\n\x03typ\x18\x04\x20\x01(\x0e2\
+    \x0b.Album.TypeB\x02\x18\0\x12\x11\n\x05label\x18\x05\x20\x01(\tB\x02\
+    \x18\0\x12\x17\n\x04date\x18\x06\x20\x01(\x0b2\x05.DateB\x02\x18\0\x12\
+    \x16\n\npopularity\x18\x07\x20\x01(\x11B\x02\x18\0\x12\x11\n\x05genre\
+    \x18\x08\x20\x03(\tB\x02\x18\0\x12\x19\n\x05cover\x18\t\x20\x03(\x0b2\
+    \x06.ImageB\x02\x18\0\x12$\n\x0bexternal_id\x18\n\x20\x03(\x0b2\x0b.Exte\
+    rnalIdB\x02\x18\0\x12\x17\n\x04disc\x18\x0b\x20\x03(\x0b2\x05.DiscB\x02\
+    \x18\0\x12\x12\n\x06review\x18\x0c\x20\x03(\tB\x02\x18\0\x12!\n\tcopyrig\
+    ht\x18\r\x20\x03(\x0b2\n.CopyrightB\x02\x18\0\x12%\n\x0brestriction\x18\
+    \x0e\x20\x03(\x0b2\x0c.RestrictionB\x02\x18\0\x12\x1b\n\x07related\x18\
+    \x0f\x20\x03(\x0b2\x06.AlbumB\x02\x18\0\x12$\n\x0bsale_period\x18\x10\
+    \x20\x03(\x0b2\x0b.SalePeriodB\x02\x18\0\x12$\n\x0bcover_group\x18\x11\
+    \x20\x01(\x0b2\x0b.ImageGroupB\x02\x18\0\":\n\x04Type\x12\t\n\x05ALBUM\
+    \x10\x01\x12\n\n\x06SINGLE\x10\x02\x12\x0f\n\x0bCOMPILATION\x10\x03\x12\
+    \x06\n\x02EP\x10\x04\x1a\x02\x10\0\"\xa6\x03\n\x05Track\x12\x0f\n\x03gid\
+    \x18\x01\x20\x01(\x0cB\x02\x18\0\x12\x10\n\x04name\x18\x02\x20\x01(\tB\
+    \x02\x18\0\x12\x19\n\x05album\x18\x03\x20\x01(\x0b2\x06.AlbumB\x02\x18\0\
+    \x12\x1b\n\x06artist\x18\x04\x20\x03(\x0b2\x07.ArtistB\x02\x18\0\x12\x12\
+    \n\x06number\x18\x05\x20\x01(\x11B\x02\x18\0\x12\x17\n\x0bdisc_number\
+    \x18\x06\x20\x01(\x11B\x02\x18\0\x12\x14\n\x08duration\x18\x07\x20\x01(\
+    \x11B\x02\x18\0\x12\x16\n\npopularity\x18\x08\x20\x01(\x11B\x02\x18\0\
+    \x12\x14\n\x08explicit\x18\t\x20\x01(\x08B\x02\x18\0\x12$\n\x0bexternal_\
+    id\x18\n\x20\x03(\x0b2\x0b.ExternalIdB\x02\x18\0\x12%\n\x0brestriction\
+    \x18\x0b\x20\x03(\x0b2\x0c.RestrictionB\x02\x18\0\x12\x1c\n\x04file\x18\
+    \x0c\x20\x03(\x0b2\n.AudioFileB\x02\x18\0\x12\x1f\n\x0balternative\x18\r\
+    \x20\x03(\x0b2\x06.TrackB\x02\x18\0\x12$\n\x0bsale_period\x18\x0e\x20\
+    \x03(\x0b2\x0b.SalePeriodB\x02\x18\0\x12\x1f\n\x07preview\x18\x0f\x20\
+    \x03(\x0b2\n.AudioFileB\x02\x18\0\"\x9d\x01\n\x05Image\x12\x13\n\x07file\
+    _id\x18\x01\x20\x01(\x0cB\x02\x18\0\x12\x1d\n\x04size\x18\x02\x20\x01(\
+    \x0e2\x0b.Image.SizeB\x02\x18\0\x12\x11\n\x05width\x18\x03\x20\x01(\x11B\
+    \x02\x18\0\x12\x12\n\x06height\x18\x04\x20\x01(\x11B\x02\x18\0\"9\n\x04S\
+    ize\x12\x0b\n\x07DEFAULT\x10\0\x12\t\n\x05SMALL\x10\x01\x12\t\n\x05LARGE\
+    \x10\x02\x12\n\n\x06XLARGE\x10\x03\x1a\x02\x10\0\"'\n\nImageGroup\x12\
+    \x19\n\x05image\x18\x01\x20\x03(\x0b2\x06.ImageB\x02\x18\0\"d\n\tBiograp\
+    hy\x12\x10\n\x04text\x18\x01\x20\x01(\tB\x02\x18\0\x12\x1c\n\x08portrait\
+    \x18\x02\x20\x03(\x0b2\x06.ImageB\x02\x18\0\x12'\n\x0eportrait_group\x18\
+    \x03\x20\x03(\x0b2\x0b.ImageGroupB\x02\x18\0\"G\n\x04Disc\x12\x12\n\x06n\
+    umber\x18\x01\x20\x01(\x11B\x02\x18\0\x12\x10\n\x04name\x18\x02\x20\x01(\
+    \tB\x02\x18\0\x12\x19\n\x05track\x18\x03\x20\x03(\x0b2\x06.TrackB\x02\
+    \x18\0\"Y\n\tCopyright\x12\x20\n\x03typ\x18\x01\x20\x01(\x0e2\x0f.Copyri\
+    ght.TypeB\x02\x18\0\x12\x10\n\x04text\x18\x02\x20\x01(\tB\x02\x18\0\"\
+    \x18\n\x04Type\x12\x05\n\x01P\x10\0\x12\x05\n\x01C\x10\x01\x1a\x02\x10\0\
+    \"\xa7\x01\n\x0bRestriction\x12\x1d\n\x11countries_allowed\x18\x02\x20\
+    \x01(\tB\x02\x18\0\x12\x1f\n\x13countries_forbidden\x18\x03\x20\x01(\tB\
+    \x02\x18\0\x12\"\n\x03typ\x18\x04\x20\x01(\x0e2\x11.Restriction.TypeB\
+    \x02\x18\0\x12\x19\n\rcatalogue_str\x18\x05\x20\x03(\tB\x02\x18\0\"\x19\
+    \n\x04Type\x12\r\n\tSTREAMING\x10\0\x1a\x02\x10\0\"e\n\nSalePeriod\x12%\
+    \n\x0brestriction\x18\x01\x20\x03(\x0b2\x0c.RestrictionB\x02\x18\0\x12\
+    \x18\n\x05start\x18\x02\x20\x01(\x0b2\x05.DateB\x02\x18\0\x12\x16\n\x03e\
+    nd\x18\x03\x20\x01(\x0b2\x05.DateB\x02\x18\0\"-\n\nExternalId\x12\x0f\n\
+    \x03typ\x18\x01\x20\x01(\tB\x02\x18\0\x12\x0e\n\x02id\x18\x02\x20\x01(\t\
+    B\x02\x18\0\"\x9f\x02\n\tAudioFile\x12\x13\n\x07file_id\x18\x01\x20\x01(\
+    \x0cB\x02\x18\0\x12%\n\x06format\x18\x02\x20\x01(\x0e2\x11.AudioFile.For\
+    matB\x02\x18\0\"\xd5\x01\n\x06Format\x12\x11\n\rOGG_VORBIS_96\x10\0\x12\
+    \x12\n\x0eOGG_VORBIS_160\x10\x01\x12\x12\n\x0eOGG_VORBIS_320\x10\x02\x12\
+    \x0b\n\x07MP3_256\x10\x03\x12\x0b\n\x07MP3_320\x10\x04\x12\x0b\n\x07MP3_\
+    160\x10\x05\x12\n\n\x06MP3_96\x10\x06\x12\x0f\n\x0bMP3_160_ENC\x10\x07\
+    \x12\n\n\x06OTHER2\x10\x08\x12\n\n\x06OTHER3\x10\t\x12\x0b\n\x07AAC_160\
+    \x10\n\x12\x0b\n\x07AAC_320\x10\x0b\x12\n\n\x06OTHER4\x10\x0c\x12\n\n\
+    \x06OTHER5\x10\r\x1a\x02\x10\0B\0b\x06proto2\
 ";
 
 static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {

+ 4 - 3
protocol/src/pubsub.rs

@@ -273,9 +273,10 @@ impl ::protobuf::reflect::ProtobufValue for Subscription {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0cpubsub.proto\"Y\n\x0cSubscription\x12\x10\n\x03uri\x18\x01\x20\x01\
-    (\tR\x03uri\x12\x16\n\x06expiry\x18\x02\x20\x01(\x05R\x06expiry\x12\x1f\
-    \n\x0bstatus_code\x18\x03\x20\x01(\x05R\nstatusCode\
+    \n\x0cpubsub.proto\x12\0\"L\n\x0cSubscription\x12\x0f\n\x03uri\x18\x01\
+    \x20\x01(\tB\x02\x18\0\x12\x12\n\x06expiry\x18\x02\x20\x01(\x05B\x02\x18\
+    \0\x12\x17\n\x0bstatus_code\x18\x03\x20\x01(\x05B\x02\x18\0B\0b\x06proto\
+    2\
 ";
 
 static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {

+ 70 - 70
protocol/src/spirc.rs

@@ -4002,79 +4002,79 @@ impl ::protobuf::reflect::ProtobufValue for PlayStatus {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0bspirc.proto\"\xfa\x03\n\x05Frame\x12\x18\n\x07version\x18\x01\x20\
-    \x01(\rR\x07version\x12\x14\n\x05ident\x18\x02\x20\x01(\tR\x05ident\x12)\
-    \n\x10protocol_version\x18\x03\x20\x01(\tR\x0fprotocolVersion\x12\x15\n\
-    \x06seq_nr\x18\x04\x20\x01(\rR\x05seqNr\x12\x1e\n\x03typ\x18\x05\x20\x01\
-    (\x0e2\x0c.MessageTypeR\x03typ\x12/\n\x0cdevice_state\x18\x07\x20\x01(\
-    \x0b2\x0c.DeviceStateR\x0bdeviceState\x12\"\n\x07goodbye\x18\x0b\x20\x01\
-    (\x0b2\x08.GoodbyeR\x07goodbye\x12\x1c\n\x05state\x18\x0c\x20\x01(\x0b2\
-    \x06.StateR\x05state\x12\x1a\n\x08position\x18\r\x20\x01(\rR\x08position\
-    \x12\x16\n\x06volume\x18\x0e\x20\x01(\rR\x06volume\x12&\n\x0fstate_updat\
-    e_id\x18\x11\x20\x01(\x03R\rstateUpdateId\x12\x1c\n\trecipient\x18\x12\
-    \x20\x03(\tR\trecipient\x120\n\x14context_player_state\x18\x13\x20\x01(\
-    \x0cR\x12contextPlayerState\x12\x19\n\x08new_name\x18\x14\x20\x01(\tR\
-    \x07newName\x12%\n\x08metadata\x18\x19\x20\x01(\x0b2\t.MetadataR\x08meta\
-    data\"\x88\x03\n\x0bDeviceState\x12\x1d\n\nsw_version\x18\x01\x20\x01(\t\
-    R\tswVersion\x12\x1b\n\tis_active\x18\n\x20\x01(\x08R\x08isActive\x12\
-    \x19\n\x08can_play\x18\x0b\x20\x01(\x08R\x07canPlay\x12\x16\n\x06volume\
-    \x18\x0c\x20\x01(\rR\x06volume\x12\x12\n\x04name\x18\r\x20\x01(\tR\x04na\
-    me\x12\x1d\n\nerror_code\x18\x0e\x20\x01(\rR\terrorCode\x12(\n\x10became\
-    _active_at\x18\x0f\x20\x01(\x03R\x0ebecameActiveAt\x12#\n\rerror_message\
-    \x18\x10\x20\x01(\tR\x0cerrorMessage\x12/\n\x0ccapabilities\x18\x11\x20\
-    \x03(\x0b2\x0b.CapabilityR\x0ccapabilities\x120\n\x14context_player_erro\
-    r\x18\x14\x20\x01(\tR\x12contextPlayerError\x12%\n\x08metadata\x18\x19\
-    \x20\x03(\x0b2\t.MetadataR\x08metadata\"m\n\nCapability\x12!\n\x03typ\
-    \x18\x01\x20\x01(\x0e2\x0f.CapabilityTypeR\x03typ\x12\x1a\n\x08intValue\
-    \x18\x02\x20\x03(\x03R\x08intValue\x12\x20\n\x0bstringValue\x18\x03\x20\
-    \x03(\tR\x0bstringValue\"!\n\x07Goodbye\x12\x16\n\x06reason\x18\x01\x20\
-    \x01(\tR\x06reason\"\xa1\x04\n\x05State\x12\x1f\n\x0bcontext_uri\x18\x02\
-    \x20\x01(\tR\ncontextUri\x12\x14\n\x05index\x18\x03\x20\x01(\rR\x05index\
-    \x12\x1f\n\x0bposition_ms\x18\x04\x20\x01(\rR\npositionMs\x12#\n\x06stat\
-    us\x18\x05\x20\x01(\x0e2\x0b.PlayStatusR\x06status\x120\n\x14position_me\
-    asured_at\x18\x07\x20\x01(\x04R\x12positionMeasuredAt\x12/\n\x13context_\
-    description\x18\x08\x20\x01(\tR\x12contextDescription\x12\x18\n\x07shuff\
-    le\x18\r\x20\x01(\x08R\x07shuffle\x12\x16\n\x06repeat\x18\x0e\x20\x01(\
-    \x08R\x06repeat\x12,\n\x12last_command_ident\x18\x14\x20\x01(\tR\x10last\
-    CommandIdent\x12,\n\x12last_command_msgid\x18\x15\x20\x01(\rR\x10lastCom\
-    mandMsgid\x122\n\x15playing_from_fallback\x18\x18\x20\x01(\x08R\x13playi\
-    ngFromFallback\x12\x10\n\x03row\x18\x19\x20\x01(\rR\x03row\x12.\n\x13pla\
-    ying_track_index\x18\x1a\x20\x01(\rR\x11playingTrackIndex\x12\x1f\n\x05t\
-    rack\x18\x1b\x20\x03(\x0b2\t.TrackRefR\x05track\x12\x13\n\x02ad\x18\x1c\
-    \x20\x01(\x0b2\x03.AdR\x02ad\"`\n\x08TrackRef\x12\x10\n\x03gid\x18\x01\
-    \x20\x01(\x0cR\x03gid\x12\x10\n\x03uri\x18\x02\x20\x01(\tR\x03uri\x12\
-    \x16\n\x06queued\x18\x03\x20\x01(\x08R\x06queued\x12\x18\n\x07context\
-    \x18\x04\x20\x01(\tR\x07context\"\xfa\x01\n\x02Ad\x12\x12\n\x04next\x18\
-    \x01\x20\x01(\x05R\x04next\x12\x17\n\x07ogg_fid\x18\x02\x20\x01(\x0cR\
-    \x06oggFid\x12\x1b\n\timage_fid\x18\x03\x20\x01(\x0cR\x08imageFid\x12\
-    \x1a\n\x08duration\x18\x04\x20\x01(\x05R\x08duration\x12\x1b\n\tclick_ur\
-    l\x18\x05\x20\x01(\tR\x08clickUrl\x12%\n\x0eimpression_url\x18\x06\x20\
-    \x01(\tR\rimpressionUrl\x12\x18\n\x07product\x18\x07\x20\x01(\tR\x07prod\
-    uct\x12\x1e\n\nadvertiser\x18\x08\x20\x01(\tR\nadvertiser\x12\x10\n\x03g\
-    id\x18\t\x20\x01(\x0cR\x03gid\":\n\x08Metadata\x12\x12\n\x04type\x18\x01\
-    \x20\x01(\tR\x04type\x12\x1a\n\x08metadata\x18\x02\x20\x01(\tR\x08metada\
-    ta*\x8d\x04\n\x0bMessageType\x12\x15\n\x11kMessageTypeHello\x10\x01\x12\
-    \x17\n\x13kMessageTypeGoodbye\x10\x02\x12\x15\n\x11kMessageTypeProbe\x10\
-    \x03\x12\x16\n\x12kMessageTypeNotify\x10\n\x12\x14\n\x10kMessageTypeLoad\
-    \x10\x14\x12\x14\n\x10kMessageTypePlay\x10\x15\x12\x15\n\x11kMessageType\
-    Pause\x10\x16\x12\x19\n\x15kMessageTypePlayPause\x10\x17\x12\x14\n\x10kM\
-    essageTypeSeek\x10\x18\x12\x14\n\x10kMessageTypePrev\x10\x19\x12\x14\n\
-    \x10kMessageTypeNext\x10\x1a\x12\x16\n\x12kMessageTypeVolume\x10\x1b\x12\
-    \x17\n\x13kMessageTypeShuffle\x10\x1c\x12\x16\n\x12kMessageTypeRepeat\
-    \x10\x1d\x12\x1a\n\x16kMessageTypeVolumeDown\x10\x1f\x12\x18\n\x14kMessa\
-    geTypeVolumeUp\x10\x20\x12\x17\n\x13kMessageTypeReplace\x10!\x12\x16\n\
-    \x12kMessageTypeLogout\x10\"\x12\x16\n\x12kMessageTypeAction\x10#\x12\
-    \x16\n\x12kMessageTypeRename\x10$\x12\x1f\n\x1akMessageTypeUpdateMetadat\
-    a\x10\x80\x01*\xb2\x02\n\x0eCapabilityType\x12\x16\n\x12kSupportedContex\
-    ts\x10\x01\x12\x10\n\x0ckCanBePlayer\x10\x02\x12\x14\n\x10kRestrictToLoc\
-    al\x10\x03\x12\x0f\n\x0bkDeviceType\x10\x04\x12\x14\n\x10kGaiaEqConnectI\
-    d\x10\x05\x12\x13\n\x0fkSupportsLogout\x10\x06\x12\x11\n\rkIsObservable\
+    \n\x0bspirc.proto\x12\0\"\x99\x03\n\x05Frame\x12\x13\n\x07version\x18\
+    \x01\x20\x01(\rB\x02\x18\0\x12\x11\n\x05ident\x18\x02\x20\x01(\tB\x02\
+    \x18\0\x12\x1c\n\x10protocol_version\x18\x03\x20\x01(\tB\x02\x18\0\x12\
+    \x12\n\x06seq_nr\x18\x04\x20\x01(\rB\x02\x18\0\x12\x1d\n\x03typ\x18\x05\
+    \x20\x01(\x0e2\x0c.MessageTypeB\x02\x18\0\x12&\n\x0cdevice_state\x18\x07\
+    \x20\x01(\x0b2\x0c.DeviceStateB\x02\x18\0\x12\x1d\n\x07goodbye\x18\x0b\
+    \x20\x01(\x0b2\x08.GoodbyeB\x02\x18\0\x12\x19\n\x05state\x18\x0c\x20\x01\
+    (\x0b2\x06.StateB\x02\x18\0\x12\x14\n\x08position\x18\r\x20\x01(\rB\x02\
+    \x18\0\x12\x12\n\x06volume\x18\x0e\x20\x01(\rB\x02\x18\0\x12\x1b\n\x0fst\
+    ate_update_id\x18\x11\x20\x01(\x03B\x02\x18\0\x12\x15\n\trecipient\x18\
+    \x12\x20\x03(\tB\x02\x18\0\x12\x20\n\x14context_player_state\x18\x13\x20\
+    \x01(\x0cB\x02\x18\0\x12\x14\n\x08new_name\x18\x14\x20\x01(\tB\x02\x18\0\
+    \x12\x1f\n\x08metadata\x18\x19\x20\x01(\x0b2\t.MetadataB\x02\x18\0\"\xb3\
+    \x02\n\x0bDeviceState\x12\x16\n\nsw_version\x18\x01\x20\x01(\tB\x02\x18\
+    \0\x12\x15\n\tis_active\x18\n\x20\x01(\x08B\x02\x18\0\x12\x14\n\x08can_p\
+    lay\x18\x0b\x20\x01(\x08B\x02\x18\0\x12\x12\n\x06volume\x18\x0c\x20\x01(\
+    \rB\x02\x18\0\x12\x10\n\x04name\x18\r\x20\x01(\tB\x02\x18\0\x12\x16\n\ne\
+    rror_code\x18\x0e\x20\x01(\rB\x02\x18\0\x12\x1c\n\x10became_active_at\
+    \x18\x0f\x20\x01(\x03B\x02\x18\0\x12\x19\n\rerror_message\x18\x10\x20\
+    \x01(\tB\x02\x18\0\x12%\n\x0ccapabilities\x18\x11\x20\x03(\x0b2\x0b.Capa\
+    bilityB\x02\x18\0\x12\x20\n\x14context_player_error\x18\x14\x20\x01(\tB\
+    \x02\x18\0\x12\x1f\n\x08metadata\x18\x19\x20\x03(\x0b2\t.MetadataB\x02\
+    \x18\0\"]\n\nCapability\x12\x20\n\x03typ\x18\x01\x20\x01(\x0e2\x0f.Capab\
+    ilityTypeB\x02\x18\0\x12\x14\n\x08intValue\x18\x02\x20\x03(\x03B\x02\x18\
+    \0\x12\x17\n\x0bstringValue\x18\x03\x20\x03(\tB\x02\x18\0\"\x1d\n\x07Goo\
+    dbye\x12\x12\n\x06reason\x18\x01\x20\x01(\tB\x02\x18\0\"\xa1\x03\n\x05St\
+    ate\x12\x17\n\x0bcontext_uri\x18\x02\x20\x01(\tB\x02\x18\0\x12\x11\n\x05\
+    index\x18\x03\x20\x01(\rB\x02\x18\0\x12\x17\n\x0bposition_ms\x18\x04\x20\
+    \x01(\rB\x02\x18\0\x12\x1f\n\x06status\x18\x05\x20\x01(\x0e2\x0b.PlaySta\
+    tusB\x02\x18\0\x12\x20\n\x14position_measured_at\x18\x07\x20\x01(\x04B\
+    \x02\x18\0\x12\x1f\n\x13context_description\x18\x08\x20\x01(\tB\x02\x18\
+    \0\x12\x13\n\x07shuffle\x18\r\x20\x01(\x08B\x02\x18\0\x12\x12\n\x06repea\
+    t\x18\x0e\x20\x01(\x08B\x02\x18\0\x12\x1e\n\x12last_command_ident\x18\
+    \x14\x20\x01(\tB\x02\x18\0\x12\x1e\n\x12last_command_msgid\x18\x15\x20\
+    \x01(\rB\x02\x18\0\x12!\n\x15playing_from_fallback\x18\x18\x20\x01(\x08B\
+    \x02\x18\0\x12\x0f\n\x03row\x18\x19\x20\x01(\rB\x02\x18\0\x12\x1f\n\x13p\
+    laying_track_index\x18\x1a\x20\x01(\rB\x02\x18\0\x12\x1c\n\x05track\x18\
+    \x1b\x20\x03(\x0b2\t.TrackRefB\x02\x18\0\x12\x13\n\x02ad\x18\x1c\x20\x01\
+    (\x0b2\x03.AdB\x02\x18\0\"U\n\x08TrackRef\x12\x0f\n\x03gid\x18\x01\x20\
+    \x01(\x0cB\x02\x18\0\x12\x0f\n\x03uri\x18\x02\x20\x01(\tB\x02\x18\0\x12\
+    \x12\n\x06queued\x18\x03\x20\x01(\x08B\x02\x18\0\x12\x13\n\x07context\
+    \x18\x04\x20\x01(\tB\x02\x18\0\"\xc9\x01\n\x02Ad\x12\x10\n\x04next\x18\
+    \x01\x20\x01(\x05B\x02\x18\0\x12\x13\n\x07ogg_fid\x18\x02\x20\x01(\x0cB\
+    \x02\x18\0\x12\x15\n\timage_fid\x18\x03\x20\x01(\x0cB\x02\x18\0\x12\x14\
+    \n\x08duration\x18\x04\x20\x01(\x05B\x02\x18\0\x12\x15\n\tclick_url\x18\
+    \x05\x20\x01(\tB\x02\x18\0\x12\x1a\n\x0eimpression_url\x18\x06\x20\x01(\
+    \tB\x02\x18\0\x12\x13\n\x07product\x18\x07\x20\x01(\tB\x02\x18\0\x12\x16\
+    \n\nadvertiser\x18\x08\x20\x01(\tB\x02\x18\0\x12\x0f\n\x03gid\x18\t\x20\
+    \x01(\x0cB\x02\x18\0\"2\n\x08Metadata\x12\x10\n\x04type\x18\x01\x20\x01(\
+    \tB\x02\x18\0\x12\x14\n\x08metadata\x18\x02\x20\x01(\tB\x02\x18\0*\x91\
+    \x04\n\x0bMessageType\x12\x15\n\x11kMessageTypeHello\x10\x01\x12\x17\n\
+    \x13kMessageTypeGoodbye\x10\x02\x12\x15\n\x11kMessageTypeProbe\x10\x03\
+    \x12\x16\n\x12kMessageTypeNotify\x10\n\x12\x14\n\x10kMessageTypeLoad\x10\
+    \x14\x12\x14\n\x10kMessageTypePlay\x10\x15\x12\x15\n\x11kMessageTypePaus\
+    e\x10\x16\x12\x19\n\x15kMessageTypePlayPause\x10\x17\x12\x14\n\x10kMessa\
+    geTypeSeek\x10\x18\x12\x14\n\x10kMessageTypePrev\x10\x19\x12\x14\n\x10kM\
+    essageTypeNext\x10\x1a\x12\x16\n\x12kMessageTypeVolume\x10\x1b\x12\x17\n\
+    \x13kMessageTypeShuffle\x10\x1c\x12\x16\n\x12kMessageTypeRepeat\x10\x1d\
+    \x12\x1a\n\x16kMessageTypeVolumeDown\x10\x1f\x12\x18\n\x14kMessageTypeVo\
+    lumeUp\x10\x20\x12\x17\n\x13kMessageTypeReplace\x10!\x12\x16\n\x12kMessa\
+    geTypeLogout\x10\"\x12\x16\n\x12kMessageTypeAction\x10#\x12\x16\n\x12kMe\
+    ssageTypeRename\x10$\x12\x1f\n\x1akMessageTypeUpdateMetadata\x10\x80\x01\
+    \x1a\x02\x10\0*\xb6\x02\n\x0eCapabilityType\x12\x16\n\x12kSupportedConte\
+    xts\x10\x01\x12\x10\n\x0ckCanBePlayer\x10\x02\x12\x14\n\x10kRestrictToLo\
+    cal\x10\x03\x12\x0f\n\x0bkDeviceType\x10\x04\x12\x14\n\x10kGaiaEqConnect\
+    Id\x10\x05\x12\x13\n\x0fkSupportsLogout\x10\x06\x12\x11\n\rkIsObservable\
     \x10\x07\x12\x10\n\x0ckVolumeSteps\x10\x08\x12\x13\n\x0fkSupportedTypes\
     \x10\t\x12\x10\n\x0ckCommandAcks\x10\n\x12\x13\n\x0fkSupportsRename\x10\
     \x0b\x12\x0b\n\x07kHidden\x10\x0c\x12\x17\n\x13kSupportsPlaylistV2\x10\r\
-    \x12\x1d\n\x19kSupportsExternalEpisodes\x10\x0e*d\n\nPlayStatus\x12\x13\
-    \n\x0fkPlayStatusStop\x10\0\x12\x13\n\x0fkPlayStatusPlay\x10\x01\x12\x14\
-    \n\x10kPlayStatusPause\x10\x02\x12\x16\n\x12kPlayStatusLoading\x10\x03\
+    \x12\x1d\n\x19kSupportsExternalEpisodes\x10\x0e\x1a\x02\x10\0*h\n\nPlayS\
+    tatus\x12\x13\n\x0fkPlayStatusStop\x10\0\x12\x13\n\x0fkPlayStatusPlay\
+    \x10\x01\x12\x14\n\x10kPlayStatusPause\x10\x02\x12\x16\n\x12kPlayStatusL\
+    oading\x10\x03\x1a\x02\x10\0B\0b\x06proto2\
 ";
 
 static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {

+ 0 - 1
src/lib.rs

@@ -2,7 +2,6 @@
 #![cfg_attr(feature = "cargo-clippy", allow(unused_io_amount))]
 
 extern crate base64;
-extern crate crypto;
 extern crate futures;
 extern crate hyper;
 extern crate num_bigint;

+ 10 - 6
src/main.rs

@@ -1,4 +1,3 @@
-extern crate crypto;
 extern crate env_logger;
 extern crate futures;
 extern crate getopts;
@@ -11,9 +10,10 @@ extern crate tokio_io;
 extern crate tokio_process;
 extern crate tokio_signal;
 extern crate url;
+extern crate sha1;
+extern crate hex;
 
-use crypto::digest::Digest;
-use crypto::sha1::Sha1;
+use sha1::{Sha1, Digest};
 use env_logger::LogBuilder;
 use futures::sync::mpsc::UnboundedReceiver;
 use futures::{Async, Future, Poll, Stream};
@@ -44,9 +44,7 @@ mod player_event_handler;
 use player_event_handler::run_program_on_events;
 
 fn device_id(name: &str) -> String {
-    let mut h = Sha1::new();
-    h.input_str(name);
-    h.result_str()
+    hex::encode(Sha1::digest(name.as_bytes()))
 }
 
 fn usage(program: &str, opts: &getopts::Options) -> String {
@@ -202,6 +200,10 @@ fn setup(args: &[String]) -> Setup {
     let backend = audio_backend::find(backend_name).expect("Invalid backend");
 
     let device = matches.opt_str("device");
+    if device == Some("?".into()) {
+        backend(device);
+        exit(0);
+    }
 
     let mixer_name = matches.opt_str("mixer");
     let mixer = mixer::find(mixer_name.as_ref()).expect("Invalid mixer");
@@ -445,6 +447,8 @@ impl Future for Main {
                 if !self.shutdown {
                     if let Some(ref spirc) = self.spirc {
                         spirc.shutdown();
+                    } else {
+                        return Ok(Async::Ready(()));
                     }
                     self.shutdown = true;
                 } else {

Неке датотеке нису приказане због велике количине промена