Browse Source

Merge branch 'dev' into gst1.0-2020

Sasha Hilton 3 years ago
parent
commit
732bb1ce82

+ 1 - 1
.travis.yml

@@ -1,6 +1,6 @@
 language: rust
 language: rust
 rust:
 rust:
-  - 1.33.0
+  - 1.40.0
   - stable
   - stable
   - beta
   - beta
   - nightly
   - nightly

+ 1 - 1
COMPILING.md

@@ -13,7 +13,7 @@ curl https://sh.rustup.rs -sSf | sh
 
 
 Follow any prompts it gives you to install Rust. Once that’s done, Rust's standard tools should be setup and ready to use.
 Follow any prompts it gives you to install Rust. Once that’s done, Rust's standard tools should be setup and ready to use.
 
 
-*Note: The current minimum required Rust version is 1.33.0*
+*Note: The current minimum required Rust version at the time of writing is 1.40.0, you can find the current minimum version specified in the `.travis.yml` file.*
 
 
 #### Additional Rust tools - `rustfmt`
 #### Additional Rust tools - `rustfmt`
 To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt), which is installed by default with `rustup` these days, else it can be installed manually with:
 To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt), which is installed by default with `rustup` these days, else it can be installed manually with:

File diff suppressed because it is too large
+ 244 - 277
Cargo.lock


+ 10 - 10
Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = "librespot"
 name = "librespot"
-version = "0.1.1"
+version = "0.1.2"
 authors = ["Librespot Org"]
 authors = ["Librespot Org"]
 license = "MIT"
 license = "MIT"
 description = "An open source client library for Spotify, with support for Spotify Connect"
 description = "An open source client library for Spotify, with support for Spotify Connect"
@@ -22,22 +22,22 @@ doc = false
 
 
 [dependencies.librespot-audio]
 [dependencies.librespot-audio]
 path = "audio"
 path = "audio"
-version = "0.1.1"
+version = "0.1.2"
 [dependencies.librespot-connect]
 [dependencies.librespot-connect]
 path = "connect"
 path = "connect"
-version = "0.1.1"
+version = "0.1.2"
 [dependencies.librespot-core]
 [dependencies.librespot-core]
 path = "core"
 path = "core"
-version = "0.1.1"
+version = "0.1.2"
 [dependencies.librespot-metadata]
 [dependencies.librespot-metadata]
 path = "metadata"
 path = "metadata"
-version = "0.1.1"
+version = "0.1.2"
 [dependencies.librespot-playback]
 [dependencies.librespot-playback]
 path = "playback"
 path = "playback"
-version = "0.1.1"
+version = "0.1.2"
 [dependencies.librespot-protocol]
 [dependencies.librespot-protocol]
 path = "protocol"
 path = "protocol"
-version = "0.1.1"
+version = "0.1.2"
 
 
 [dependencies]
 [dependencies]
 base64 = "0.10"
 base64 = "0.10"
@@ -47,7 +47,7 @@ getopts = "0.2"
 hyper = "0.11"
 hyper = "0.11"
 log = "0.4"
 log = "0.4"
 num-bigint = "0.2"
 num-bigint = "0.2"
-protobuf = "2.8.1"
+protobuf = "~2.14.0"
 rand = "0.7"
 rand = "0.7"
 rpassword = "3.0"
 rpassword = "3.0"
 tokio-core = "0.1"
 tokio-core = "0.1"
@@ -77,9 +77,9 @@ default = ["librespot-playback/rodio-backend"]
 [package.metadata.deb]
 [package.metadata.deb]
 maintainer = "librespot-org"
 maintainer = "librespot-org"
 copyright = "2018 Paul Liétar"
 copyright = "2018 Paul Liétar"
-license_file = ["LICENSE", "4"]
+license-file = ["LICENSE", "4"]
 depends = "$auto"
 depends = "$auto"
-extended_description = """\
+extended-description = """\
 librespot is an open source client library for Spotify. It enables applications \
 librespot is an open source client library for Spotify. It enables applications \
 to use Spotify's service, without using the official but closed-source \
 to use Spotify's service, without using the official but closed-source \
 libspotify. Additionally, it will provide extra features which are not \
 libspotify. Additionally, it will provide extra features which are not \

+ 3 - 3
audio/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = "librespot-audio"
 name = "librespot-audio"
-version = "0.1.1"
+version = "0.1.2"
 authors = ["Paul Lietar <paul@lietar.net>"]
 authors = ["Paul Lietar <paul@lietar.net>"]
 description="The audio fetching and processing logic for librespot"
 description="The audio fetching and processing logic for librespot"
 license="MIT"
 license="MIT"
@@ -8,7 +8,7 @@ edition = "2018"
 
 
 [dependencies.librespot-core]
 [dependencies.librespot-core]
 path = "../core"
 path = "../core"
-version = "0.1.1"
+version = "0.1.2"
 
 
 [dependencies]
 [dependencies]
 bit-set = "0.5"
 bit-set = "0.5"
@@ -23,7 +23,7 @@ tempfile = "3.1"
 aes-ctr = "0.3"
 aes-ctr = "0.3"
 
 
 librespot-tremor = { version = "0.1.0", optional = true }
 librespot-tremor = { version = "0.1.0", optional = true }
-vorbis = { version ="0.1.0", optional = true }
+vorbis = { version ="0.0.14", optional = true }
 
 
 [features]
 [features]
 with-tremor = ["librespot-tremor"]
 with-tremor = ["librespot-tremor"]

+ 5 - 5
connect/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = "librespot-connect"
 name = "librespot-connect"
-version = "0.1.1"
+version = "0.1.2"
 authors = ["Paul Lietar <paul@lietar.net>"]
 authors = ["Paul Lietar <paul@lietar.net>"]
 description="The discovery and Spotify Connect logic for librespot"
 description="The discovery and Spotify Connect logic for librespot"
 license="MIT"
 license="MIT"
@@ -8,13 +8,13 @@ edition = "2018"
 
 
 [dependencies.librespot-core]
 [dependencies.librespot-core]
 path = "../core"
 path = "../core"
-version = "0.1.1"
+version = "0.1.2"
 [dependencies.librespot-playback]
 [dependencies.librespot-playback]
 path = "../playback"
 path = "../playback"
-version = "0.1.1"
+version = "0.1.2"
 [dependencies.librespot-protocol]
 [dependencies.librespot-protocol]
 path = "../protocol"
 path = "../protocol"
-version = "0.1.1"
+version = "0.1.2"
 
 
 [dependencies]
 [dependencies]
 base64 = "0.10"
 base64 = "0.10"
@@ -22,7 +22,7 @@ futures = "0.1"
 hyper = "0.11"
 hyper = "0.11"
 log = "0.4"
 log = "0.4"
 num-bigint = "0.2"
 num-bigint = "0.2"
-protobuf = "2.8.1"
+protobuf = "~2.14.0"
 rand = "0.7"
 rand = "0.7"
 serde = "1.0"
 serde = "1.0"
 serde_derive = "1.0"
 serde_derive = "1.0"

+ 91 - 32
connect/src/spirc.rs

@@ -14,10 +14,12 @@ use crate::playback::mixer::Mixer;
 use crate::playback::player::{Player, PlayerEvent, PlayerEventChannel};
 use crate::playback::player::{Player, PlayerEvent, PlayerEventChannel};
 use crate::protocol;
 use crate::protocol;
 use crate::protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef};
 use crate::protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef};
+
 use librespot_core::config::ConnectConfig;
 use librespot_core::config::ConnectConfig;
 use librespot_core::mercury::MercuryError;
 use librespot_core::mercury::MercuryError;
 use librespot_core::session::Session;
 use librespot_core::session::Session;
 use librespot_core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError};
 use librespot_core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError};
+use librespot_core::util::url_encode;
 use librespot_core::util::SeqGenerator;
 use librespot_core::util::SeqGenerator;
 use librespot_core::version;
 use librespot_core::version;
 use librespot_core::volume::Volume;
 use librespot_core::volume::Volume;
@@ -249,7 +251,8 @@ impl Spirc {
         let ident = session.device_id().to_owned();
         let ident = session.device_id().to_owned();
 
 
         // Uri updated in response to issue #288
         // Uri updated in response to issue #288
-        let uri = format!("hm://remote/user/{}/", session.username());
+        debug!("canonical_username: {}", url_encode(&session.username()));
+        let uri = format!("hm://remote/user/{}/", url_encode(&session.username()));
 
 
         let subscription = session.mercury().subscribe(&uri as &str);
         let subscription = session.mercury().subscribe(&uri as &str);
         let subscription = subscription
         let subscription = subscription
@@ -454,8 +457,8 @@ impl SpircTask {
             Ok(dur) => dur,
             Ok(dur) => dur,
             Err(err) => err.duration(),
             Err(err) => err.duration(),
         };
         };
-        ((dur.as_secs() as i64 + self.session.time_delta()) * 1000
-            + (dur.subsec_nanos() / 1000_000) as i64)
+        (dur.as_secs() as i64 + self.session.time_delta()) * 1000
+            + (dur.subsec_nanos() / 1000_000) as i64
     }
     }
 
 
     fn ensure_mixer_started(&mut self) {
     fn ensure_mixer_started(&mut self) {
@@ -621,24 +624,8 @@ impl SpircTask {
                             self.play_status = SpircPlayStatus::Stopped;
                             self.play_status = SpircPlayStatus::Stopped;
                         }
                         }
                     },
                     },
-                    PlayerEvent::TimeToPreloadNextTrack { .. } => match self.play_status {
-                        SpircPlayStatus::Paused {
-                            ref mut preloading_of_next_track_triggered,
-                            ..
-                        }
-                        | SpircPlayStatus::Playing {
-                            ref mut preloading_of_next_track_triggered,
-                            ..
-                        } => {
-                            *preloading_of_next_track_triggered = true;
-                            if let Some(track_id) = self.preview_next_track() {
-                                self.player.preload(track_id);
-                            }
-                        }
-                        SpircPlayStatus::LoadingPause { .. }
-                        | SpircPlayStatus::LoadingPlay { .. }
-                        | SpircPlayStatus::Stopped => (),
-                    },
+                    PlayerEvent::TimeToPreloadNextTrack { .. } => self.handle_preload_next_track(),
+                    PlayerEvent::Unavailable { track_id, .. } => self.handle_unavailable(track_id),
                     _ => (),
                     _ => (),
                 }
                 }
             }
             }
@@ -777,6 +764,7 @@ impl SpircTask {
                 } = self.play_status
                 } = self.play_status
                 {
                 {
                     if preloading_of_next_track_triggered {
                     if preloading_of_next_track_triggered {
+                        // Get the next track_id in the playlist
                         if let Some(track_id) = self.preview_next_track() {
                         if let Some(track_id) = self.preview_next_track() {
                             self.player.preload(track_id);
                             self.player.preload(track_id);
                         }
                         }
@@ -790,7 +778,11 @@ impl SpircTask {
             }
             }
 
 
             MessageType::kMessageTypeNotify => {
             MessageType::kMessageTypeNotify => {
-                if self.device.get_is_active() && frame.get_device_state().get_is_active() {
+                if self.device.get_is_active()
+                    && frame.get_device_state().get_is_active()
+                    && self.device.get_became_active_at()
+                        <= frame.get_device_state().get_became_active_at()
+                {
                     self.device.set_is_active(false);
                     self.device.set_is_active(false);
                     self.state.set_status(PlayStatus::kPlayStatusStop);
                     self.state.set_status(PlayStatus::kPlayStatusStop);
                     self.player.stop();
                     self.player.stop();
@@ -904,6 +896,50 @@ impl SpircTask {
             .and_then(|(track_id, _)| Some(track_id))
             .and_then(|(track_id, _)| Some(track_id))
     }
     }
 
 
+    fn handle_preload_next_track(&mut self) {
+        // Requests the player thread to preload the next track
+        match self.play_status {
+            SpircPlayStatus::Paused {
+                ref mut preloading_of_next_track_triggered,
+                ..
+            }
+            | SpircPlayStatus::Playing {
+                ref mut preloading_of_next_track_triggered,
+                ..
+            } => {
+                *preloading_of_next_track_triggered = true;
+                if let Some(track_id) = self.preview_next_track() {
+                    self.player.preload(track_id);
+                }
+            }
+            SpircPlayStatus::LoadingPause { .. }
+            | SpircPlayStatus::LoadingPlay { .. }
+            | SpircPlayStatus::Stopped => (),
+        }
+    }
+
+    // Mark unavailable tracks so we can skip them later
+    fn handle_unavailable(&mut self, track_id: SpotifyId) {
+        let unavailables = self.get_track_index_for_spotify_id(&track_id, 0);
+        for &index in unavailables.iter() {
+            debug_assert_eq!(self.state.get_track()[index].get_gid(), track_id.to_raw());
+            let mut unplayable_track_ref = TrackRef::new();
+            unplayable_track_ref.set_gid(self.state.get_track()[index].get_gid().to_vec());
+            // Misuse context field to flag the track
+            unplayable_track_ref.set_context(String::from("NonPlayable"));
+            std::mem::swap(
+                &mut self.state.mut_track()[index],
+                &mut unplayable_track_ref,
+            );
+            debug!(
+                "Marked <{:?}> at {:?} as NonPlayable",
+                self.state.get_track()[index],
+                index,
+            );
+        }
+        self.handle_preload_next_track();
+    }
+
     fn handle_next(&mut self) {
     fn handle_next(&mut self) {
         let mut new_index = self.consume_queued_track() as u32;
         let mut new_index = self.consume_queued_track() as u32;
         let mut continue_playing = true;
         let mut continue_playing = true;
@@ -919,7 +955,7 @@ impl SpircTask {
         if (context_uri.starts_with("spotify:station:")
         if (context_uri.starts_with("spotify:station:")
             || context_uri.starts_with("spotify:dailymix:")
             || context_uri.starts_with("spotify:dailymix:")
             // spotify:user:xxx:collection
             // spotify:user:xxx:collection
-            || context_uri.starts_with(&format!("spotify:user:{}:collection",self.session.username())))
+            || context_uri.starts_with(&format!("spotify:user:{}:collection",url_encode(&self.session.username()))))
             && ((self.state.get_track().len() as u32) - new_index) < CONTEXT_FETCH_THRESHOLD
             && ((self.state.get_track().len() as u32) - new_index) < CONTEXT_FETCH_THRESHOLD
         {
         {
             self.context_fut = self.resolve_station(&context_uri);
             self.context_fut = self.resolve_station(&context_uri);
@@ -1137,10 +1173,32 @@ impl SpircTask {
         })
         })
     }
     }
 
 
+    // Helper to find corresponding index(s) for track_id
+    fn get_track_index_for_spotify_id(
+        &self,
+        track_id: &SpotifyId,
+        start_index: usize,
+    ) -> Vec<usize> {
+        let index: Vec<usize> = self.state.get_track()[start_index..]
+            .iter()
+            .enumerate()
+            .filter(|&(_, track_ref)| track_ref.get_gid() == track_id.to_raw())
+            .map(|(idx, _)| start_index + idx)
+            .collect();
+        // Sanity check
+        debug_assert!(!index.is_empty());
+        index
+    }
+
+    // Broken out here so we can refactor this later when we move to SpotifyObjectID or similar
+    fn track_ref_is_unavailable(&self, track_ref: &TrackRef) -> bool {
+        track_ref.get_context() == "NonPlayable"
+    }
+
     fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> {
     fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> {
-        let tracks_len = self.state.get_track().len() as u32;
+        let tracks_len = self.state.get_track().len();
 
 
-        let mut new_playlist_index = index;
+        let mut new_playlist_index = index as usize;
 
 
         if new_playlist_index >= tracks_len {
         if new_playlist_index >= tracks_len {
             new_playlist_index = 0;
             new_playlist_index = 0;
@@ -1152,14 +1210,15 @@ impl SpircTask {
         // tracks in each frame either have a gid or uri (that may or may not be a valid track)
         // tracks in each frame either have a gid or uri (that may or may not be a valid track)
         // E.g - context based frames sometimes contain tracks with <spotify:meta:page:>
         // E.g - context based frames sometimes contain tracks with <spotify:meta:page:>
 
 
-        let mut track_ref = self.state.get_track()[new_playlist_index as usize].clone();
+        let mut track_ref = self.state.get_track()[new_playlist_index].clone();
         let mut track_id = self.get_spotify_id_for_track(&track_ref);
         let mut track_id = self.get_spotify_id_for_track(&track_ref);
-        while track_id.is_err() || track_id.unwrap().audio_type == SpotifyAudioType::NonPlayable {
+        while self.track_ref_is_unavailable(&track_ref)
+            || track_id.is_err()
+            || track_id.unwrap().audio_type == SpotifyAudioType::NonPlayable
+        {
             warn!(
             warn!(
                 "Skipping track <{:?}> at position [{}] of {}",
                 "Skipping track <{:?}> at position [{}] of {}",
-                track_ref.get_uri(),
-                new_playlist_index,
-                tracks_len
+                track_ref, new_playlist_index, tracks_len
             );
             );
 
 
             new_playlist_index += 1;
             new_playlist_index += 1;
@@ -1171,12 +1230,12 @@ impl SpircTask {
                 warn!("No playable track found in state: {:?}", self.state);
                 warn!("No playable track found in state: {:?}", self.state);
                 return None;
                 return None;
             }
             }
-            track_ref = self.state.get_track()[index as usize].clone();
+            track_ref = self.state.get_track()[new_playlist_index].clone();
             track_id = self.get_spotify_id_for_track(&track_ref);
             track_id = self.get_spotify_id_for_track(&track_ref);
         }
         }
 
 
         match track_id {
         match track_id {
-            Ok(track_id) => Some((track_id, new_playlist_index)),
+            Ok(track_id) => Some((track_id, new_playlist_index as u32)),
             Err(_) => None,
             Err(_) => None,
         }
         }
     }
     }

+ 3 - 3
core/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = "librespot-core"
 name = "librespot-core"
-version = "0.1.1"
+version = "0.1.2"
 authors = ["Paul Lietar <paul@lietar.net>"]
 authors = ["Paul Lietar <paul@lietar.net>"]
 build = "build.rs"
 build = "build.rs"
 description="The core functionality provided by librespot"
 description="The core functionality provided by librespot"
@@ -9,7 +9,7 @@ edition = "2018"
 
 
 [dependencies.librespot-protocol]
 [dependencies.librespot-protocol]
 path = "../protocol"
 path = "../protocol"
-version = "0.1.1"
+version = "0.1.2"
 
 
 [dependencies]
 [dependencies]
 base64 = "0.10"
 base64 = "0.10"
@@ -25,7 +25,7 @@ log = "0.4"
 num-bigint = "0.2"
 num-bigint = "0.2"
 num-integer = "0.1"
 num-integer = "0.1"
 num-traits = "0.2"
 num-traits = "0.2"
-protobuf = "2.8.1"
+protobuf = "~2.14.0"
 rand = "0.7"
 rand = "0.7"
 serde = "1.0"
 serde = "1.0"
 serde_derive = "1.0"
 serde_derive = "1.0"

+ 2 - 1
core/src/mercury/mod.rs

@@ -1,4 +1,5 @@
 use crate::protocol;
 use crate::protocol;
+use crate::util::url_encode;
 use byteorder::{BigEndian, ByteOrder};
 use byteorder::{BigEndian, ByteOrder};
 use bytes::Bytes;
 use bytes::Bytes;
 use futures::sync::{mpsc, oneshot};
 use futures::sync::{mpsc, oneshot};
@@ -192,7 +193,7 @@ impl MercuryManager {
         let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap();
         let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap();
 
 
         let response = MercuryResponse {
         let response = MercuryResponse {
-            uri: header.get_uri().to_owned(),
+            uri: url_encode(header.get_uri()).to_owned(),
             status_code: header.get_status_code(),
             status_code: header.get_status_code(),
             payload: pending.parts,
             payload: pending.parts,
         };
         };

+ 15 - 0
core/src/util/mod.rs

@@ -12,6 +12,21 @@ pub fn rand_vec<G: Rng>(rng: &mut G, size: usize) -> Vec<u8> {
         .collect()
         .collect()
 }
 }
 
 
+pub fn url_encode(inp: &str) -> String {
+    let mut encoded = String::new();
+
+    for c in inp.as_bytes().iter() {
+        match *c as char {
+            'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' | ':' | '/' => {
+                encoded.push(*c as char)
+            }
+            c => encoded.push_str(format!("%{:02X}", c as u32).as_str()),
+        };
+    }
+
+    encoded
+}
+
 pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
 pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
     let mut base = base.clone();
     let mut base = base.clone();
     let mut exp = exp.clone();
     let mut exp = exp.clone();

+ 4 - 4
metadata/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = "librespot-metadata"
 name = "librespot-metadata"
-version = "0.1.1"
+version = "0.1.2"
 authors = ["Paul Lietar <paul@lietar.net>"]
 authors = ["Paul Lietar <paul@lietar.net>"]
 description="The metadata logic for librespot"
 description="The metadata logic for librespot"
 license="MIT"
 license="MIT"
@@ -10,12 +10,12 @@ edition = "2018"
 byteorder = "1.3"
 byteorder = "1.3"
 futures = "0.1"
 futures = "0.1"
 linear-map = "1.2"
 linear-map = "1.2"
-protobuf = "2.8.1"
+protobuf = "~2.14.0"
 log = "0.4"
 log = "0.4"
 
 
 [dependencies.librespot-core]
 [dependencies.librespot-core]
 path = "../core"
 path = "../core"
-version = "0.1.1"
+version = "0.1.2"
 [dependencies.librespot-protocol]
 [dependencies.librespot-protocol]
 path = "../protocol"
 path = "../protocol"
-version = "0.1.1"
+version = "0.1.2"

+ 4 - 4
playback/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = "librespot-playback"
 name = "librespot-playback"
-version = "0.1.1"
+version = "0.1.2"
 authors = ["Sasha Hilton <sashahilton00@gmail.com>"]
 authors = ["Sasha Hilton <sashahilton00@gmail.com>"]
 description="The audio playback logic for librespot"
 description="The audio playback logic for librespot"
 license="MIT"
 license="MIT"
@@ -8,13 +8,13 @@ edition = "2018"
 
 
 [dependencies.librespot-audio]
 [dependencies.librespot-audio]
 path = "../audio"
 path = "../audio"
-version = "0.1.1"
+version = "0.1.2"
 [dependencies.librespot-core]
 [dependencies.librespot-core]
 path = "../core"
 path = "../core"
-version = "0.1.1"
+version = "0.1.2"
 [dependencies.librespot-metadata]
 [dependencies.librespot-metadata]
 path = "../metadata"
 path = "../metadata"
-version = "0.1.1"
+version = "0.1.2"
 
 
 [dependencies]
 [dependencies]
 futures = "0.1"
 futures = "0.1"

+ 54 - 17
playback/src/audio_backend/alsa.rs

@@ -1,12 +1,20 @@
 use super::{Open, Sink};
 use super::{Open, Sink};
 use alsa::device_name::HintIter;
 use alsa::device_name::HintIter;
-use alsa::pcm::{Access, Format, HwParams, PCM};
+use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
 use alsa::{Direction, Error, ValueOr};
 use alsa::{Direction, Error, ValueOr};
+use std::cmp::min;
 use std::ffi::CString;
 use std::ffi::CString;
 use std::io;
 use std::io;
 use std::process::exit;
 use std::process::exit;
 
 
-pub struct AlsaSink(Option<PCM>, String);
+const PREFERED_PERIOD_SIZE: Frames = 5512; // Period of roughly 125ms
+const BUFFERED_PERIODS: Frames = 4;
+
+pub struct AlsaSink {
+    pcm: Option<PCM>,
+    device: String,
+    buffer: Vec<i16>,
+}
 
 
 fn list_outputs() {
 fn list_outputs() {
     for t in &["pcm", "ctl", "hwdep"] {
     for t in &["pcm", "ctl", "hwdep"] {
@@ -25,8 +33,9 @@ fn list_outputs() {
     }
     }
 }
 }
 
 
-fn open_device(dev_name: &str) -> Result<(PCM), Box<Error>> {
+fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box<Error>> {
     let pcm = PCM::new(dev_name, Direction::Playback, false)?;
     let pcm = PCM::new(dev_name, Direction::Playback, false)?;
+    let mut period_size = PREFERED_PERIOD_SIZE;
     // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8
     // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8
     // latency = period_size * periods / (rate * bytes_per_frame)
     // latency = period_size * periods / (rate * bytes_per_frame)
     // For 16 Bit stereo data, one frame has a length of four bytes.
     // For 16 Bit stereo data, one frame has a length of four bytes.
@@ -41,7 +50,8 @@ fn open_device(dev_name: &str) -> Result<(PCM), Box<Error>> {
         hwp.set_format(Format::s16())?;
         hwp.set_format(Format::s16())?;
         hwp.set_rate(44100, ValueOr::Nearest)?;
         hwp.set_rate(44100, ValueOr::Nearest)?;
         hwp.set_channels(2)?;
         hwp.set_channels(2)?;
-        hwp.set_buffer_size_near(22050)?; // ~ 0.5s latency
+        period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?;
+        hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?;
         pcm.hw_params(&hwp)?;
         pcm.hw_params(&hwp)?;
 
 
         let swp = pcm.sw_params_current()?;
         let swp = pcm.sw_params_current()?;
@@ -49,7 +59,7 @@ fn open_device(dev_name: &str) -> Result<(PCM), Box<Error>> {
         pcm.sw_params(&swp)?;
         pcm.sw_params(&swp)?;
     }
     }
 
 
-    Ok(pcm)
+    Ok((pcm, period_size))
 }
 }
 
 
 impl Open for AlsaSink {
 impl Open for AlsaSink {
@@ -67,16 +77,24 @@ impl Open for AlsaSink {
         }
         }
         .to_string();
         .to_string();
 
 
-        AlsaSink(None, name)
+        AlsaSink {
+            pcm: None,
+            device: name,
+            buffer: vec![],
+        }
     }
     }
 }
 }
 
 
 impl Sink for AlsaSink {
 impl Sink for AlsaSink {
     fn start(&mut self) -> io::Result<()> {
     fn start(&mut self) -> io::Result<()> {
-        if self.0.is_none() {
-            let pcm = open_device(&self.1);
+        if self.pcm.is_none() {
+            let pcm = open_device(&self.device);
             match pcm {
             match pcm {
-                Ok(p) => self.0 = Some(p),
+                Ok((p, period_size)) => {
+                    self.pcm = Some(p);
+                    // Create a buffer for all samples for a full period
+                    self.buffer = Vec::with_capacity((period_size * 2) as usize);
+                }
                 Err(e) => {
                 Err(e) => {
                     error!("Alsa error PCM open {}", e);
                     error!("Alsa error PCM open {}", e);
                     return Err(io::Error::new(
                     return Err(io::Error::new(
@@ -92,20 +110,39 @@ impl Sink for AlsaSink {
 
 
     fn stop(&mut self) -> io::Result<()> {
     fn stop(&mut self) -> io::Result<()> {
         {
         {
-            let pcm = self.0.as_ref().unwrap();
+            let pcm = self.pcm.as_mut().unwrap();
+            // Write any leftover data in the period buffer
+            // before draining the actual buffer
+            let io = pcm.io_i16().unwrap();
+            match io.writei(&self.buffer[..]) {
+                Ok(_) => (),
+                Err(err) => pcm.try_recover(err, false).unwrap(),
+            }
             pcm.drain().unwrap();
             pcm.drain().unwrap();
         }
         }
-        self.0 = None;
+        self.pcm = None;
         Ok(())
         Ok(())
     }
     }
 
 
     fn write(&mut self, data: &[i16]) -> io::Result<()> {
     fn write(&mut self, data: &[i16]) -> io::Result<()> {
-        let pcm = self.0.as_mut().unwrap();
-        let io = pcm.io_i16().unwrap();
-
-        match io.writei(&data) {
-            Ok(_) => (),
-            Err(err) => pcm.try_recover(err, false).unwrap(),
+        let mut processed_data = 0;
+        while processed_data < data.len() {
+            let data_to_buffer = min(
+                self.buffer.capacity() - self.buffer.len(),
+                data.len() - processed_data,
+            );
+            self.buffer
+                .extend_from_slice(&data[processed_data..processed_data + data_to_buffer]);
+            processed_data += data_to_buffer;
+            if self.buffer.len() == self.buffer.capacity() {
+                let pcm = self.pcm.as_mut().unwrap();
+                let io = pcm.io_i16().unwrap();
+                match io.writei(&self.buffer) {
+                    Ok(_) => (),
+                    Err(err) => pcm.try_recover(err, false).unwrap(),
+                }
+                self.buffer.clear();
+            }
         }
         }
 
 
         Ok(())
         Ok(())

+ 139 - 18
playback/src/player.rs

@@ -33,6 +33,15 @@ pub struct Player {
     play_request_id_generator: SeqGenerator<u64>,
     play_request_id_generator: SeqGenerator<u64>,
 }
 }
 
 
+#[derive(PartialEq, Debug, Clone, Copy)]
+pub enum SinkStatus {
+    Running,
+    Closed,
+    TemporarilyClosed,
+}
+
+pub type SinkEventCallback = Box<dyn Fn(SinkStatus) + Send>;
+
 struct PlayerInternal {
 struct PlayerInternal {
     session: Session,
     session: Session,
     config: PlayerConfig,
     config: PlayerConfig,
@@ -41,7 +50,8 @@ struct PlayerInternal {
     state: PlayerState,
     state: PlayerState,
     preload: PlayerPreload,
     preload: PlayerPreload,
     sink: Box<dyn Sink>,
     sink: Box<dyn Sink>,
-    sink_running: bool,
+    sink_status: SinkStatus,
+    sink_event_callback: Option<SinkEventCallback>,
     audio_filter: Option<Box<dyn AudioFilter + Send>>,
     audio_filter: Option<Box<dyn AudioFilter + Send>>,
     event_senders: Vec<futures::sync::mpsc::UnboundedSender<PlayerEvent>>,
     event_senders: Vec<futures::sync::mpsc::UnboundedSender<PlayerEvent>>,
 }
 }
@@ -61,6 +71,7 @@ enum PlayerCommand {
     Stop,
     Stop,
     Seek(u32),
     Seek(u32),
     AddEventSender(futures::sync::mpsc::UnboundedSender<PlayerEvent>),
     AddEventSender(futures::sync::mpsc::UnboundedSender<PlayerEvent>),
+    SetSinkEventCallback(Option<SinkEventCallback>),
     EmitVolumeSetEvent(u16),
     EmitVolumeSetEvent(u16),
 }
 }
 
 
@@ -123,6 +134,11 @@ pub enum PlayerEvent {
         play_request_id: u64,
         play_request_id: u64,
         track_id: SpotifyId,
         track_id: SpotifyId,
     },
     },
+    // The player was unable to load the requested track.
+    Unavailable {
+        play_request_id: u64,
+        track_id: SpotifyId,
+    },
     // The mixer volume was set to a new level.
     // The mixer volume was set to a new level.
     VolumeSet {
     VolumeSet {
         volume: u16,
         volume: u16,
@@ -136,6 +152,9 @@ impl PlayerEvent {
             Loading {
             Loading {
                 play_request_id, ..
                 play_request_id, ..
             }
             }
+            | Unavailable {
+                play_request_id, ..
+            }
             | Started {
             | Started {
                 play_request_id, ..
                 play_request_id, ..
             }
             }
@@ -232,7 +251,8 @@ impl Player {
                 state: PlayerState::Stopped,
                 state: PlayerState::Stopped,
                 preload: PlayerPreload::None,
                 preload: PlayerPreload::None,
                 sink: sink_builder(),
                 sink: sink_builder(),
-                sink_running: false,
+                sink_status: SinkStatus::Closed,
+                sink_event_callback: None,
                 audio_filter: audio_filter,
                 audio_filter: audio_filter,
                 event_senders: [event_sender].to_vec(),
                 event_senders: [event_sender].to_vec(),
             };
             };
@@ -308,6 +328,10 @@ impl Player {
         Box::new(result)
         Box::new(result)
     }
     }
 
 
+    pub fn set_sink_event_callback(&self, callback: Option<SinkEventCallback>) {
+        self.command(PlayerCommand::SetSinkEventCallback(callback));
+    }
+
     pub fn emit_volume_set_event(&self, volume: u16) {
     pub fn emit_volume_set_event(&self, volume: u16) {
         self.command(PlayerCommand::EmitVolumeSetEvent(volume));
         self.command(PlayerCommand::EmitVolumeSetEvent(volume));
     }
     }
@@ -398,6 +422,7 @@ impl PlayerState {
         }
         }
     }
     }
 
 
+    #[allow(dead_code)]
     fn is_stopped(&self) -> bool {
     fn is_stopped(&self) -> bool {
         use self::PlayerState::*;
         use self::PlayerState::*;
         match *self {
         match *self {
@@ -406,6 +431,14 @@ impl PlayerState {
         }
         }
     }
     }
 
 
+    fn is_loading(&self) -> bool {
+        use self::PlayerState::*;
+        match *self {
+            Loading { .. } => true,
+            _ => false,
+        }
+    }
+
     fn decoder(&mut self) -> Option<&mut Decoder> {
     fn decoder(&mut self) -> Option<&mut Decoder> {
         use self::PlayerState::*;
         use self::PlayerState::*;
         match *self {
         match *self {
@@ -748,8 +781,12 @@ impl Future for PlayerInternal {
                     }
                     }
                     Ok(Async::NotReady) => (),
                     Ok(Async::NotReady) => (),
                     Err(_) => {
                     Err(_) => {
-                        self.handle_player_stop();
-                        assert!(self.state.is_stopped());
+                        warn!("Unable to load <{:?}>\nSkipping to next track", track_id);
+                        assert!(self.state.is_loading());
+                        self.send_event(PlayerEvent::EndOfTrack {
+                            track_id,
+                            play_request_id,
+                        })
                     }
                     }
                 }
                 }
             }
             }
@@ -769,7 +806,21 @@ impl Future for PlayerInternal {
                     }
                     }
                     Ok(Async::NotReady) => (),
                     Ok(Async::NotReady) => (),
                     Err(_) => {
                     Err(_) => {
+                        debug!("Unable to preload {:?}", track_id);
                         self.preload = PlayerPreload::None;
                         self.preload = PlayerPreload::None;
+                        // Let Spirc know that the track was unavailable.
+                        if let PlayerState::Playing {
+                            play_request_id, ..
+                        }
+                        | PlayerState::Paused {
+                            play_request_id, ..
+                        } = self.state
+                        {
+                            self.send_event(PlayerEvent::Unavailable {
+                                track_id,
+                                play_request_id,
+                            });
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -882,20 +933,41 @@ impl PlayerInternal {
     }
     }
 
 
     fn ensure_sink_running(&mut self) {
     fn ensure_sink_running(&mut self) {
-        if !self.sink_running {
+        if self.sink_status != SinkStatus::Running {
             trace!("== Starting sink ==");
             trace!("== Starting sink ==");
+            if let Some(callback) = &mut self.sink_event_callback {
+                callback(SinkStatus::Running);
+            }
             match self.sink.start() {
             match self.sink.start() {
-                Ok(()) => self.sink_running = true,
+                Ok(()) => self.sink_status = SinkStatus::Running,
                 Err(err) => error!("Could not start audio: {}", err),
                 Err(err) => error!("Could not start audio: {}", err),
             }
             }
         }
         }
     }
     }
 
 
-    fn ensure_sink_stopped(&mut self) {
-        if self.sink_running {
-            trace!("== Stopping sink ==");
-            self.sink.stop().unwrap();
-            self.sink_running = false;
+    fn ensure_sink_stopped(&mut self, temporarily: bool) {
+        match self.sink_status {
+            SinkStatus::Running => {
+                trace!("== Stopping sink ==");
+                self.sink.stop().unwrap();
+                self.sink_status = if temporarily {
+                    SinkStatus::TemporarilyClosed
+                } else {
+                    SinkStatus::Closed
+                };
+                if let Some(callback) = &mut self.sink_event_callback {
+                    callback(self.sink_status);
+                }
+            }
+            SinkStatus::TemporarilyClosed => {
+                if !temporarily {
+                    self.sink_status = SinkStatus::Closed;
+                    if let Some(callback) = &mut self.sink_event_callback {
+                        callback(SinkStatus::Closed);
+                    }
+                }
+            }
+            SinkStatus::Closed => (),
         }
         }
     }
     }
 
 
@@ -921,7 +993,7 @@ impl PlayerInternal {
                 play_request_id,
                 play_request_id,
                 ..
                 ..
             } => {
             } => {
-                self.ensure_sink_stopped();
+                self.ensure_sink_stopped(false);
                 self.send_event(PlayerEvent::Stopped {
                 self.send_event(PlayerEvent::Stopped {
                     track_id,
                     track_id,
                     play_request_id,
                     play_request_id,
@@ -968,7 +1040,7 @@ impl PlayerInternal {
         {
         {
             self.state.playing_to_paused();
             self.state.playing_to_paused();
 
 
-            self.ensure_sink_stopped();
+            self.ensure_sink_stopped(false);
             let position_ms = Self::position_pcm_to_ms(stream_position_pcm);
             let position_ms = Self::position_pcm_to_ms(stream_position_pcm);
             self.send_event(PlayerEvent::Paused {
             self.send_event(PlayerEvent::Paused {
                 track_id,
                 track_id,
@@ -997,7 +1069,7 @@ impl PlayerInternal {
 
 
                     if let Err(err) = self.sink.write(&packet.data()) {
                     if let Err(err) = self.sink.write(&packet.data()) {
                         error!("Could not write audio: {}", err);
                         error!("Could not write audio: {}", err);
-                        self.ensure_sink_stopped();
+                        self.ensure_sink_stopped(false);
                     }
                     }
                 }
                 }
             }
             }
@@ -1055,7 +1127,7 @@ impl PlayerInternal {
                 suggested_to_preload_next_track: false,
                 suggested_to_preload_next_track: false,
             };
             };
         } else {
         } else {
-            self.ensure_sink_stopped();
+            self.ensure_sink_stopped(false);
 
 
             self.state = PlayerState::Paused {
             self.state = PlayerState::Paused {
                 track_id: track_id,
                 track_id: track_id,
@@ -1086,7 +1158,7 @@ impl PlayerInternal {
         position_ms: u32,
         position_ms: u32,
     ) {
     ) {
         if !self.config.gapless {
         if !self.config.gapless {
-            self.ensure_sink_stopped();
+            self.ensure_sink_stopped(play);
         }
         }
         // emit the correct player event
         // emit the correct player event
         match self.state {
         match self.state {
@@ -1254,7 +1326,7 @@ impl PlayerInternal {
 
 
         // We need to load the track - either from scratch or by completing a preload.
         // We need to load the track - either from scratch or by completing a preload.
         // In any case we go into a Loading state to load the track.
         // In any case we go into a Loading state to load the track.
-        self.ensure_sink_stopped();
+        self.ensure_sink_stopped(play);
 
 
         self.send_event(PlayerEvent::Loading {
         self.send_event(PlayerEvent::Loading {
             track_id,
             track_id,
@@ -1302,7 +1374,6 @@ impl PlayerInternal {
     fn handle_command_preload(&mut self, track_id: SpotifyId) {
     fn handle_command_preload(&mut self, track_id: SpotifyId) {
         debug!("Preloading track");
         debug!("Preloading track");
         let mut preload_track = true;
         let mut preload_track = true;
-
         // check whether the track is already loaded somewhere or being loaded.
         // check whether the track is already loaded somewhere or being loaded.
         if let PlayerPreload::Loading {
         if let PlayerPreload::Loading {
             track_id: currently_loading,
             track_id: currently_loading,
@@ -1436,6 +1507,8 @@ impl PlayerInternal {
 
 
             PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender),
             PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender),
 
 
+            PlayerCommand::SetSinkEventCallback(callback) => self.sink_event_callback = callback,
+
             PlayerCommand::EmitVolumeSetEvent(volume) => {
             PlayerCommand::EmitVolumeSetEvent(volume) => {
                 self.send_event(PlayerEvent::VolumeSet { volume })
                 self.send_event(PlayerEvent::VolumeSet { volume })
             }
             }
@@ -1540,6 +1613,9 @@ impl ::std::fmt::Debug for PlayerCommand {
             PlayerCommand::Stop => f.debug_tuple("Stop").finish(),
             PlayerCommand::Stop => f.debug_tuple("Stop").finish(),
             PlayerCommand::Seek(position) => f.debug_tuple("Seek").field(&position).finish(),
             PlayerCommand::Seek(position) => f.debug_tuple("Seek").field(&position).finish(),
             PlayerCommand::AddEventSender(_) => f.debug_tuple("AddEventSender").finish(),
             PlayerCommand::AddEventSender(_) => f.debug_tuple("AddEventSender").finish(),
+            PlayerCommand::SetSinkEventCallback(_) => {
+                f.debug_tuple("SetSinkEventCallback").finish()
+            }
             PlayerCommand::EmitVolumeSetEvent(volume) => {
             PlayerCommand::EmitVolumeSetEvent(volume) => {
                 f.debug_tuple("VolumeSet").field(&volume).finish()
                 f.debug_tuple("VolumeSet").field(&volume).finish()
             }
             }
@@ -1547,6 +1623,51 @@ impl ::std::fmt::Debug for PlayerCommand {
     }
     }
 }
 }
 
 
+impl ::std::fmt::Debug for PlayerState {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        use PlayerState::*;
+        match *self {
+            Stopped => f.debug_struct("Stopped").finish(),
+            Loading {
+                track_id,
+                play_request_id,
+                ..
+            } => f
+                .debug_struct("Loading")
+                .field("track_id", &track_id)
+                .field("play_request_id", &play_request_id)
+                .finish(),
+            Paused {
+                track_id,
+                play_request_id,
+                ..
+            } => f
+                .debug_struct("Paused")
+                .field("track_id", &track_id)
+                .field("play_request_id", &play_request_id)
+                .finish(),
+            Playing {
+                track_id,
+                play_request_id,
+                ..
+            } => f
+                .debug_struct("Playing")
+                .field("track_id", &track_id)
+                .field("play_request_id", &play_request_id)
+                .finish(),
+            EndOfTrack {
+                track_id,
+                play_request_id,
+                ..
+            } => f
+                .debug_struct("EndOfTrack")
+                .field("track_id", &track_id)
+                .field("play_request_id", &play_request_id)
+                .finish(),
+            Invalid => f.debug_struct("Invalid").finish(),
+        }
+    }
+}
 struct Subfile<T: Read + Seek> {
 struct Subfile<T: Read + Seek> {
     stream: T,
     stream: T,
     offset: u64,
     offset: u64,

+ 4 - 4
protocol/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 [package]
 name = "librespot-protocol"
 name = "librespot-protocol"
-version = "0.1.1"
+version = "0.1.2"
 authors = ["Paul Liétar <paul@lietar.net>"]
 authors = ["Paul Liétar <paul@lietar.net>"]
 build = "build.rs"
 build = "build.rs"
 description="The protobuf logic for communicating with Spotify servers"
 description="The protobuf logic for communicating with Spotify servers"
@@ -8,8 +8,8 @@ license="MIT"
 edition = "2018"
 edition = "2018"
 
 
 [dependencies]
 [dependencies]
-protobuf = "~2.8.1"
+protobuf = "~2.14.0"
 
 
 [build-dependencies]
 [build-dependencies]
-protobuf-codegen-pure = "~2.8.1"
-protobuf-codegen = "~2.8.1"
+protobuf-codegen-pure = "~2.14.0"
+protobuf-codegen = "~2.14.0"

+ 7 - 6
protocol/build.rs

@@ -29,20 +29,21 @@ fn main() {
         }
         }
 
 
         // Build the paths to relevant files.
         // Build the paths to relevant files.
-        let src = &format!("proto/{}.proto", name);
-        let dest = &format!("src/{}.rs", name);
-
+        let src_fname = &format!("proto/{}.proto", name);
+        let dest_fname = &format!("src/{}.rs", name);
+        let src = Path::new(src_fname);
+        let dest = Path::new(dest_fname);
         // Get the contents of the existing generated file.
         // Get the contents of the existing generated file.
         let mut existing = "".to_string();
         let mut existing = "".to_string();
-        if Path::new(dest).exists() {
+        if dest.exists() {
             // Removing CRLF line endings if present.
             // Removing CRLF line endings if present.
             existing = read_to_string(dest).unwrap().replace("\r\n", "\n");
             existing = read_to_string(dest).unwrap().replace("\r\n", "\n");
         }
         }
 
 
-        println!("Regenerating {} from {}", dest, src);
+        println!("Regenerating {} from {}", dest.display(), src.display());
 
 
         // Parse the proto files as the protobuf-codegen-pure crate does.
         // Parse the proto files as the protobuf-codegen-pure crate does.
-        let p = parse_and_typecheck(&["proto"], &[src]).expect("protoc");
+        let p = parse_and_typecheck(&[&Path::new("proto")], &[src]).expect("protoc");
         // But generate them with the protobuf-codegen crate directly.
         // But generate them with the protobuf-codegen crate directly.
         // Then we can keep the result in-memory.
         // Then we can keep the result in-memory.
         let result = protobuf_codegen::gen(&p.file_descriptors, &p.relative_paths, &customizations);
         let result = protobuf_codegen::gen(&p.file_descriptors, &p.relative_paths, &customizations);

File diff suppressed because it is too large
+ 384 - 224
protocol/src/authentication.rs


File diff suppressed because it is too large
+ 344 - 213
protocol/src/keyexchange.rs


File diff suppressed because it is too large
+ 330 - 218
protocol/src/mercury.rs


File diff suppressed because it is too large
+ 401 - 264
protocol/src/metadata.rs


File diff suppressed because it is too large
+ 324 - 222
protocol/src/playlist4changes.rs


File diff suppressed because it is too large
+ 383 - 238
protocol/src/playlist4content.rs


+ 164 - 131
protocol/src/playlist4issues.rs

@@ -1,7 +1,7 @@
-// This file is generated by rust-protobuf 2.8.1. Do not edit
+// This file is generated by rust-protobuf 2.14.0. Do not edit
 // @generated
 // @generated
 
 
-// https://github.com/Manishearth/rust-clippy/issues/702
+// https://github.com/rust-lang/rust-clippy/issues/702
 #![allow(unknown_lints)]
 #![allow(unknown_lints)]
 #![allow(clippy::all)]
 #![allow(clippy::all)]
 
 
@@ -24,9 +24,9 @@ use protobuf::ProtobufEnum as ProtobufEnum_imported_for_functions;
 
 
 /// Generated files are compatible only with the same version
 /// Generated files are compatible only with the same version
 /// of protobuf runtime.
 /// of protobuf runtime.
-const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_8_1;
+// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_14_0;
 
 
-#[derive(PartialEq,Clone,Default)]
+#[derive(PartialEq, Clone, Default)]
 pub struct ClientIssue {
 pub struct ClientIssue {
     // message fields
     // message fields
     level: ::std::option::Option<ClientIssue_Level>,
     level: ::std::option::Option<ClientIssue_Level>,
@@ -50,7 +50,6 @@ impl ClientIssue {
 
 
     // optional .ClientIssue.Level level = 1;
     // optional .ClientIssue.Level level = 1;
 
 
-
     pub fn get_level(&self) -> ClientIssue_Level {
     pub fn get_level(&self) -> ClientIssue_Level {
         self.level.unwrap_or(ClientIssue_Level::LEVEL_UNKNOWN)
         self.level.unwrap_or(ClientIssue_Level::LEVEL_UNKNOWN)
     }
     }
@@ -69,7 +68,6 @@ impl ClientIssue {
 
 
     // optional .ClientIssue.Code code = 2;
     // optional .ClientIssue.Code code = 2;
 
 
-
     pub fn get_code(&self) -> ClientIssue_Code {
     pub fn get_code(&self) -> ClientIssue_Code {
         self.code.unwrap_or(ClientIssue_Code::CODE_UNKNOWN)
         self.code.unwrap_or(ClientIssue_Code::CODE_UNKNOWN)
     }
     }
@@ -88,7 +86,6 @@ impl ClientIssue {
 
 
     // optional int32 repeatCount = 3;
     // optional int32 repeatCount = 3;
 
 
-
     pub fn get_repeatCount(&self) -> i32 {
     pub fn get_repeatCount(&self) -> i32 {
         self.repeatCount.unwrap_or(0)
         self.repeatCount.unwrap_or(0)
     }
     }
@@ -111,26 +108,44 @@ impl ::protobuf::Message for ClientIssue {
         true
         true
     }
     }
 
 
-    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+    fn merge_from(
+        &mut self,
+        is: &mut ::protobuf::CodedInputStream<'_>,
+    ) -> ::protobuf::ProtobufResult<()> {
         while !is.eof()? {
         while !is.eof()? {
             let (field_number, wire_type) = is.read_tag_unpack()?;
             let (field_number, wire_type) = is.read_tag_unpack()?;
             match field_number {
             match field_number {
-                1 => {
-                    ::protobuf::rt::read_proto2_enum_with_unknown_fields_into(wire_type, is, &mut self.level, 1, &mut self.unknown_fields)?
-                },
-                2 => {
-                    ::protobuf::rt::read_proto2_enum_with_unknown_fields_into(wire_type, is, &mut self.code, 2, &mut self.unknown_fields)?
-                },
+                1 => ::protobuf::rt::read_proto2_enum_with_unknown_fields_into(
+                    wire_type,
+                    is,
+                    &mut self.level,
+                    1,
+                    &mut self.unknown_fields,
+                )?,
+                2 => ::protobuf::rt::read_proto2_enum_with_unknown_fields_into(
+                    wire_type,
+                    is,
+                    &mut self.code,
+                    2,
+                    &mut self.unknown_fields,
+                )?,
                 3 => {
                 3 => {
                     if wire_type != ::protobuf::wire_format::WireTypeVarint {
                     if wire_type != ::protobuf::wire_format::WireTypeVarint {
-                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(
+                            wire_type,
+                        ));
                     }
                     }
                     let tmp = is.read_int32()?;
                     let tmp = is.read_int32()?;
                     self.repeatCount = ::std::option::Option::Some(tmp);
                     self.repeatCount = ::std::option::Option::Some(tmp);
-                },
+                }
                 _ => {
                 _ => {
-                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
-                },
+                    ::protobuf::rt::read_unknown_or_skip_group(
+                        field_number,
+                        wire_type,
+                        is,
+                        self.mut_unknown_fields(),
+                    )?;
+                }
             };
             };
         }
         }
         ::std::result::Result::Ok(())
         ::std::result::Result::Ok(())
@@ -154,7 +169,10 @@ impl ::protobuf::Message for ClientIssue {
         my_size
         my_size
     }
     }
 
 
-    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+    fn write_to_with_cached_sizes(
+        &self,
+        os: &mut ::protobuf::CodedOutputStream<'_>,
+    ) -> ::protobuf::ProtobufResult<()> {
         if let Some(v) = self.level {
         if let Some(v) = self.level {
             os.write_enum(1, v.value())?;
             os.write_enum(1, v.value())?;
         }
         }
@@ -199,45 +217,47 @@ impl ::protobuf::Message for ClientIssue {
     }
     }
 
 
     fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
     fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
-        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy {
-            lock: ::protobuf::lazy::ONCE_INIT,
-            ptr: 0 as *const ::protobuf::reflect::MessageDescriptor,
-        };
+        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> =
+            ::protobuf::lazy::Lazy::INIT;
         unsafe {
         unsafe {
             descriptor.get(|| {
             descriptor.get(|| {
                 let mut fields = ::std::vec::Vec::new();
                 let mut fields = ::std::vec::Vec::new();
-                fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ClientIssue_Level>>(
+                fields.push(::protobuf::reflect::accessor::make_option_accessor::<
+                    _,
+                    ::protobuf::types::ProtobufTypeEnum<ClientIssue_Level>,
+                >(
                     "level",
                     "level",
-                    |m: &ClientIssue| { &m.level },
-                    |m: &mut ClientIssue| { &mut m.level },
+                    |m: &ClientIssue| &m.level,
+                    |m: &mut ClientIssue| &mut m.level,
                 ));
                 ));
-                fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ClientIssue_Code>>(
+                fields.push(::protobuf::reflect::accessor::make_option_accessor::<
+                    _,
+                    ::protobuf::types::ProtobufTypeEnum<ClientIssue_Code>,
+                >(
                     "code",
                     "code",
-                    |m: &ClientIssue| { &m.code },
-                    |m: &mut ClientIssue| { &mut m.code },
+                    |m: &ClientIssue| &m.code,
+                    |m: &mut ClientIssue| &mut m.code,
                 ));
                 ));
-                fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>(
+                fields.push(::protobuf::reflect::accessor::make_option_accessor::<
+                    _,
+                    ::protobuf::types::ProtobufTypeInt32,
+                >(
                     "repeatCount",
                     "repeatCount",
-                    |m: &ClientIssue| { &m.repeatCount },
-                    |m: &mut ClientIssue| { &mut m.repeatCount },
+                    |m: &ClientIssue| &m.repeatCount,
+                    |m: &mut ClientIssue| &mut m.repeatCount,
                 ));
                 ));
-                ::protobuf::reflect::MessageDescriptor::new::<ClientIssue>(
+                ::protobuf::reflect::MessageDescriptor::new_pb_name::<ClientIssue>(
                     "ClientIssue",
                     "ClientIssue",
                     fields,
                     fields,
-                    file_descriptor_proto()
+                    file_descriptor_proto(),
                 )
                 )
             })
             })
         }
         }
     }
     }
 
 
     fn default_instance() -> &'static ClientIssue {
     fn default_instance() -> &'static ClientIssue {
-        static mut instance: ::protobuf::lazy::Lazy<ClientIssue> = ::protobuf::lazy::Lazy {
-            lock: ::protobuf::lazy::ONCE_INIT,
-            ptr: 0 as *const ClientIssue,
-        };
-        unsafe {
-            instance.get(ClientIssue::new)
-        }
+        static mut instance: ::protobuf::lazy::Lazy<ClientIssue> = ::protobuf::lazy::Lazy::INIT;
+        unsafe { instance.get(ClientIssue::new) }
     }
     }
 }
 }
 
 
@@ -257,12 +277,12 @@ impl ::std::fmt::Debug for ClientIssue {
 }
 }
 
 
 impl ::protobuf::reflect::ProtobufValue for ClientIssue {
 impl ::protobuf::reflect::ProtobufValue for ClientIssue {
-    fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
-        ::protobuf::reflect::ProtobufValueRef::Message(self)
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
     }
     }
 }
 }
 
 
-#[derive(Clone,PartialEq,Eq,Debug,Hash)]
+#[derive(Clone, PartialEq, Eq, Debug, Hash)]
 pub enum ClientIssue_Level {
 pub enum ClientIssue_Level {
     LEVEL_UNKNOWN = 0,
     LEVEL_UNKNOWN = 0,
     LEVEL_DEBUG = 1,
     LEVEL_DEBUG = 1,
@@ -285,7 +305,7 @@ impl ::protobuf::ProtobufEnum for ClientIssue_Level {
             3 => ::std::option::Option::Some(ClientIssue_Level::LEVEL_NOTICE),
             3 => ::std::option::Option::Some(ClientIssue_Level::LEVEL_NOTICE),
             4 => ::std::option::Option::Some(ClientIssue_Level::LEVEL_WARNING),
             4 => ::std::option::Option::Some(ClientIssue_Level::LEVEL_WARNING),
             5 => ::std::option::Option::Some(ClientIssue_Level::LEVEL_ERROR),
             5 => ::std::option::Option::Some(ClientIssue_Level::LEVEL_ERROR),
-            _ => ::std::option::Option::None
+            _ => ::std::option::Option::None,
         }
         }
     }
     }
 
 
@@ -302,20 +322,20 @@ impl ::protobuf::ProtobufEnum for ClientIssue_Level {
     }
     }
 
 
     fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
     fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
-        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::lazy::Lazy {
-            lock: ::protobuf::lazy::ONCE_INIT,
-            ptr: 0 as *const ::protobuf::reflect::EnumDescriptor,
-        };
+        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::EnumDescriptor> =
+            ::protobuf::lazy::Lazy::INIT;
         unsafe {
         unsafe {
             descriptor.get(|| {
             descriptor.get(|| {
-                ::protobuf::reflect::EnumDescriptor::new("ClientIssue_Level", file_descriptor_proto())
+                ::protobuf::reflect::EnumDescriptor::new_pb_name::<ClientIssue_Level>(
+                    "ClientIssue.Level",
+                    file_descriptor_proto(),
+                )
             })
             })
         }
         }
     }
     }
 }
 }
 
 
-impl ::std::marker::Copy for ClientIssue_Level {
-}
+impl ::std::marker::Copy for ClientIssue_Level {}
 
 
 impl ::std::default::Default for ClientIssue_Level {
 impl ::std::default::Default for ClientIssue_Level {
     fn default() -> Self {
     fn default() -> Self {
@@ -324,12 +344,12 @@ impl ::std::default::Default for ClientIssue_Level {
 }
 }
 
 
 impl ::protobuf::reflect::ProtobufValue for ClientIssue_Level {
 impl ::protobuf::reflect::ProtobufValue for ClientIssue_Level {
-    fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
-        ::protobuf::reflect::ProtobufValueRef::Enum(self.descriptor())
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Enum(self.descriptor())
     }
     }
 }
 }
 
 
-#[derive(Clone,PartialEq,Eq,Debug,Hash)]
+#[derive(Clone, PartialEq, Eq, Debug, Hash)]
 pub enum ClientIssue_Code {
 pub enum ClientIssue_Code {
     CODE_UNKNOWN = 0,
     CODE_UNKNOWN = 0,
     CODE_INDEX_OUT_OF_BOUNDS = 1,
     CODE_INDEX_OUT_OF_BOUNDS = 1,
@@ -352,7 +372,7 @@ impl ::protobuf::ProtobufEnum for ClientIssue_Code {
             3 => ::std::option::Option::Some(ClientIssue_Code::CODE_CACHED_CHANGE),
             3 => ::std::option::Option::Some(ClientIssue_Code::CODE_CACHED_CHANGE),
             4 => ::std::option::Option::Some(ClientIssue_Code::CODE_OFFLINE_CHANGE),
             4 => ::std::option::Option::Some(ClientIssue_Code::CODE_OFFLINE_CHANGE),
             5 => ::std::option::Option::Some(ClientIssue_Code::CODE_CONCURRENT_CHANGE),
             5 => ::std::option::Option::Some(ClientIssue_Code::CODE_CONCURRENT_CHANGE),
-            _ => ::std::option::Option::None
+            _ => ::std::option::Option::None,
         }
         }
     }
     }
 
 
@@ -369,20 +389,20 @@ impl ::protobuf::ProtobufEnum for ClientIssue_Code {
     }
     }
 
 
     fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
     fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
-        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::lazy::Lazy {
-            lock: ::protobuf::lazy::ONCE_INIT,
-            ptr: 0 as *const ::protobuf::reflect::EnumDescriptor,
-        };
+        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::EnumDescriptor> =
+            ::protobuf::lazy::Lazy::INIT;
         unsafe {
         unsafe {
             descriptor.get(|| {
             descriptor.get(|| {
-                ::protobuf::reflect::EnumDescriptor::new("ClientIssue_Code", file_descriptor_proto())
+                ::protobuf::reflect::EnumDescriptor::new_pb_name::<ClientIssue_Code>(
+                    "ClientIssue.Code",
+                    file_descriptor_proto(),
+                )
             })
             })
         }
         }
     }
     }
 }
 }
 
 
-impl ::std::marker::Copy for ClientIssue_Code {
-}
+impl ::std::marker::Copy for ClientIssue_Code {}
 
 
 impl ::std::default::Default for ClientIssue_Code {
 impl ::std::default::Default for ClientIssue_Code {
     fn default() -> Self {
     fn default() -> Self {
@@ -391,12 +411,12 @@ impl ::std::default::Default for ClientIssue_Code {
 }
 }
 
 
 impl ::protobuf::reflect::ProtobufValue for ClientIssue_Code {
 impl ::protobuf::reflect::ProtobufValue for ClientIssue_Code {
-    fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
-        ::protobuf::reflect::ProtobufValueRef::Enum(self.descriptor())
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Enum(self.descriptor())
     }
     }
 }
 }
 
 
-#[derive(PartialEq,Clone,Default)]
+#[derive(PartialEq, Clone, Default)]
 pub struct ClientResolveAction {
 pub struct ClientResolveAction {
     // message fields
     // message fields
     code: ::std::option::Option<ClientResolveAction_Code>,
     code: ::std::option::Option<ClientResolveAction_Code>,
@@ -419,7 +439,6 @@ impl ClientResolveAction {
 
 
     // optional .ClientResolveAction.Code code = 1;
     // optional .ClientResolveAction.Code code = 1;
 
 
-
     pub fn get_code(&self) -> ClientResolveAction_Code {
     pub fn get_code(&self) -> ClientResolveAction_Code {
         self.code.unwrap_or(ClientResolveAction_Code::CODE_UNKNOWN)
         self.code.unwrap_or(ClientResolveAction_Code::CODE_UNKNOWN)
     }
     }
@@ -438,9 +457,9 @@ impl ClientResolveAction {
 
 
     // optional .ClientResolveAction.Initiator initiator = 2;
     // optional .ClientResolveAction.Initiator initiator = 2;
 
 
-
     pub fn get_initiator(&self) -> ClientResolveAction_Initiator {
     pub fn get_initiator(&self) -> ClientResolveAction_Initiator {
-        self.initiator.unwrap_or(ClientResolveAction_Initiator::INITIATOR_UNKNOWN)
+        self.initiator
+            .unwrap_or(ClientResolveAction_Initiator::INITIATOR_UNKNOWN)
     }
     }
     pub fn clear_initiator(&mut self) {
     pub fn clear_initiator(&mut self) {
         self.initiator = ::std::option::Option::None;
         self.initiator = ::std::option::Option::None;
@@ -461,19 +480,35 @@ impl ::protobuf::Message for ClientResolveAction {
         true
         true
     }
     }
 
 
-    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+    fn merge_from(
+        &mut self,
+        is: &mut ::protobuf::CodedInputStream<'_>,
+    ) -> ::protobuf::ProtobufResult<()> {
         while !is.eof()? {
         while !is.eof()? {
             let (field_number, wire_type) = is.read_tag_unpack()?;
             let (field_number, wire_type) = is.read_tag_unpack()?;
             match field_number {
             match field_number {
-                1 => {
-                    ::protobuf::rt::read_proto2_enum_with_unknown_fields_into(wire_type, is, &mut self.code, 1, &mut self.unknown_fields)?
-                },
-                2 => {
-                    ::protobuf::rt::read_proto2_enum_with_unknown_fields_into(wire_type, is, &mut self.initiator, 2, &mut self.unknown_fields)?
-                },
+                1 => ::protobuf::rt::read_proto2_enum_with_unknown_fields_into(
+                    wire_type,
+                    is,
+                    &mut self.code,
+                    1,
+                    &mut self.unknown_fields,
+                )?,
+                2 => ::protobuf::rt::read_proto2_enum_with_unknown_fields_into(
+                    wire_type,
+                    is,
+                    &mut self.initiator,
+                    2,
+                    &mut self.unknown_fields,
+                )?,
                 _ => {
                 _ => {
-                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
-                },
+                    ::protobuf::rt::read_unknown_or_skip_group(
+                        field_number,
+                        wire_type,
+                        is,
+                        self.mut_unknown_fields(),
+                    )?;
+                }
             };
             };
         }
         }
         ::std::result::Result::Ok(())
         ::std::result::Result::Ok(())
@@ -494,7 +529,10 @@ impl ::protobuf::Message for ClientResolveAction {
         my_size
         my_size
     }
     }
 
 
-    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+    fn write_to_with_cached_sizes(
+        &self,
+        os: &mut ::protobuf::CodedOutputStream<'_>,
+    ) -> ::protobuf::ProtobufResult<()> {
         if let Some(v) = self.code {
         if let Some(v) = self.code {
             os.write_enum(1, v.value())?;
             os.write_enum(1, v.value())?;
         }
         }
@@ -536,40 +574,40 @@ impl ::protobuf::Message for ClientResolveAction {
     }
     }
 
 
     fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
     fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
-        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy {
-            lock: ::protobuf::lazy::ONCE_INIT,
-            ptr: 0 as *const ::protobuf::reflect::MessageDescriptor,
-        };
+        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> =
+            ::protobuf::lazy::Lazy::INIT;
         unsafe {
         unsafe {
             descriptor.get(|| {
             descriptor.get(|| {
                 let mut fields = ::std::vec::Vec::new();
                 let mut fields = ::std::vec::Vec::new();
-                fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ClientResolveAction_Code>>(
+                fields.push(::protobuf::reflect::accessor::make_option_accessor::<
+                    _,
+                    ::protobuf::types::ProtobufTypeEnum<ClientResolveAction_Code>,
+                >(
                     "code",
                     "code",
-                    |m: &ClientResolveAction| { &m.code },
-                    |m: &mut ClientResolveAction| { &mut m.code },
+                    |m: &ClientResolveAction| &m.code,
+                    |m: &mut ClientResolveAction| &mut m.code,
                 ));
                 ));
-                fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeEnum<ClientResolveAction_Initiator>>(
+                fields.push(::protobuf::reflect::accessor::make_option_accessor::<
+                    _,
+                    ::protobuf::types::ProtobufTypeEnum<ClientResolveAction_Initiator>,
+                >(
                     "initiator",
                     "initiator",
-                    |m: &ClientResolveAction| { &m.initiator },
-                    |m: &mut ClientResolveAction| { &mut m.initiator },
+                    |m: &ClientResolveAction| &m.initiator,
+                    |m: &mut ClientResolveAction| &mut m.initiator,
                 ));
                 ));
-                ::protobuf::reflect::MessageDescriptor::new::<ClientResolveAction>(
+                ::protobuf::reflect::MessageDescriptor::new_pb_name::<ClientResolveAction>(
                     "ClientResolveAction",
                     "ClientResolveAction",
                     fields,
                     fields,
-                    file_descriptor_proto()
+                    file_descriptor_proto(),
                 )
                 )
             })
             })
         }
         }
     }
     }
 
 
     fn default_instance() -> &'static ClientResolveAction {
     fn default_instance() -> &'static ClientResolveAction {
-        static mut instance: ::protobuf::lazy::Lazy<ClientResolveAction> = ::protobuf::lazy::Lazy {
-            lock: ::protobuf::lazy::ONCE_INIT,
-            ptr: 0 as *const ClientResolveAction,
-        };
-        unsafe {
-            instance.get(ClientResolveAction::new)
-        }
+        static mut instance: ::protobuf::lazy::Lazy<ClientResolveAction> =
+            ::protobuf::lazy::Lazy::INIT;
+        unsafe { instance.get(ClientResolveAction::new) }
     }
     }
 }
 }
 
 
@@ -588,12 +626,12 @@ impl ::std::fmt::Debug for ClientResolveAction {
 }
 }
 
 
 impl ::protobuf::reflect::ProtobufValue for ClientResolveAction {
 impl ::protobuf::reflect::ProtobufValue for ClientResolveAction {
-    fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
-        ::protobuf::reflect::ProtobufValueRef::Message(self)
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
     }
     }
 }
 }
 
 
-#[derive(Clone,PartialEq,Eq,Debug,Hash)]
+#[derive(Clone, PartialEq, Eq, Debug, Hash)]
 pub enum ClientResolveAction_Code {
 pub enum ClientResolveAction_Code {
     CODE_UNKNOWN = 0,
     CODE_UNKNOWN = 0,
     CODE_NO_ACTION = 1,
     CODE_NO_ACTION = 1,
@@ -618,7 +656,7 @@ impl ::protobuf::ProtobufEnum for ClientResolveAction_Code {
             4 => ::std::option::Option::Some(ClientResolveAction_Code::CODE_DISCARD_LOCAL_CHANGES),
             4 => ::std::option::Option::Some(ClientResolveAction_Code::CODE_DISCARD_LOCAL_CHANGES),
             5 => ::std::option::Option::Some(ClientResolveAction_Code::CODE_SEND_DUMP),
             5 => ::std::option::Option::Some(ClientResolveAction_Code::CODE_SEND_DUMP),
             6 => ::std::option::Option::Some(ClientResolveAction_Code::CODE_DISPLAY_ERROR_MESSAGE),
             6 => ::std::option::Option::Some(ClientResolveAction_Code::CODE_DISPLAY_ERROR_MESSAGE),
-            _ => ::std::option::Option::None
+            _ => ::std::option::Option::None,
         }
         }
     }
     }
 
 
@@ -636,20 +674,20 @@ impl ::protobuf::ProtobufEnum for ClientResolveAction_Code {
     }
     }
 
 
     fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
     fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
-        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::lazy::Lazy {
-            lock: ::protobuf::lazy::ONCE_INIT,
-            ptr: 0 as *const ::protobuf::reflect::EnumDescriptor,
-        };
+        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::EnumDescriptor> =
+            ::protobuf::lazy::Lazy::INIT;
         unsafe {
         unsafe {
             descriptor.get(|| {
             descriptor.get(|| {
-                ::protobuf::reflect::EnumDescriptor::new("ClientResolveAction_Code", file_descriptor_proto())
+                ::protobuf::reflect::EnumDescriptor::new_pb_name::<ClientResolveAction_Code>(
+                    "ClientResolveAction.Code",
+                    file_descriptor_proto(),
+                )
             })
             })
         }
         }
     }
     }
 }
 }
 
 
-impl ::std::marker::Copy for ClientResolveAction_Code {
-}
+impl ::std::marker::Copy for ClientResolveAction_Code {}
 
 
 impl ::std::default::Default for ClientResolveAction_Code {
 impl ::std::default::Default for ClientResolveAction_Code {
     fn default() -> Self {
     fn default() -> Self {
@@ -658,12 +696,12 @@ impl ::std::default::Default for ClientResolveAction_Code {
 }
 }
 
 
 impl ::protobuf::reflect::ProtobufValue for ClientResolveAction_Code {
 impl ::protobuf::reflect::ProtobufValue for ClientResolveAction_Code {
-    fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
-        ::protobuf::reflect::ProtobufValueRef::Enum(self.descriptor())
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Enum(self.descriptor())
     }
     }
 }
 }
 
 
-#[derive(Clone,PartialEq,Eq,Debug,Hash)]
+#[derive(Clone, PartialEq, Eq, Debug, Hash)]
 pub enum ClientResolveAction_Initiator {
 pub enum ClientResolveAction_Initiator {
     INITIATOR_UNKNOWN = 0,
     INITIATOR_UNKNOWN = 0,
     INITIATOR_SERVER = 1,
     INITIATOR_SERVER = 1,
@@ -680,7 +718,7 @@ impl ::protobuf::ProtobufEnum for ClientResolveAction_Initiator {
             0 => ::std::option::Option::Some(ClientResolveAction_Initiator::INITIATOR_UNKNOWN),
             0 => ::std::option::Option::Some(ClientResolveAction_Initiator::INITIATOR_UNKNOWN),
             1 => ::std::option::Option::Some(ClientResolveAction_Initiator::INITIATOR_SERVER),
             1 => ::std::option::Option::Some(ClientResolveAction_Initiator::INITIATOR_SERVER),
             2 => ::std::option::Option::Some(ClientResolveAction_Initiator::INITIATOR_CLIENT),
             2 => ::std::option::Option::Some(ClientResolveAction_Initiator::INITIATOR_CLIENT),
-            _ => ::std::option::Option::None
+            _ => ::std::option::Option::None,
         }
         }
     }
     }
 
 
@@ -694,20 +732,20 @@ impl ::protobuf::ProtobufEnum for ClientResolveAction_Initiator {
     }
     }
 
 
     fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
     fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
-        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::lazy::Lazy {
-            lock: ::protobuf::lazy::ONCE_INIT,
-            ptr: 0 as *const ::protobuf::reflect::EnumDescriptor,
-        };
+        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::EnumDescriptor> =
+            ::protobuf::lazy::Lazy::INIT;
         unsafe {
         unsafe {
             descriptor.get(|| {
             descriptor.get(|| {
-                ::protobuf::reflect::EnumDescriptor::new("ClientResolveAction_Initiator", file_descriptor_proto())
+                ::protobuf::reflect::EnumDescriptor::new_pb_name::<ClientResolveAction_Initiator>(
+                    "ClientResolveAction.Initiator",
+                    file_descriptor_proto(),
+                )
             })
             })
         }
         }
     }
     }
 }
 }
 
 
-impl ::std::marker::Copy for ClientResolveAction_Initiator {
-}
+impl ::std::marker::Copy for ClientResolveAction_Initiator {}
 
 
 impl ::std::default::Default for ClientResolveAction_Initiator {
 impl ::std::default::Default for ClientResolveAction_Initiator {
     fn default() -> Self {
     fn default() -> Self {
@@ -716,8 +754,8 @@ impl ::std::default::Default for ClientResolveAction_Initiator {
 }
 }
 
 
 impl ::protobuf::reflect::ProtobufValue for ClientResolveAction_Initiator {
 impl ::protobuf::reflect::ProtobufValue for ClientResolveAction_Initiator {
-    fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
-        ::protobuf::reflect::ProtobufValueRef::Enum(self.descriptor())
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Enum(self.descriptor())
     }
     }
 }
 }
 
 
@@ -743,19 +781,14 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \x10\x01\x12\x14\n\x10INITIATOR_CLIENT\x10\x02\x1a\0:\0B\0b\x06proto2\
     \x10\x01\x12\x14\n\x10INITIATOR_CLIENT\x10\x02\x1a\0:\0B\0b\x06proto2\
 ";
 ";
 
 
-static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {
-    lock: ::protobuf::lazy::ONCE_INIT,
-    ptr: 0 as *const ::protobuf::descriptor::FileDescriptorProto,
-};
+static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<
+    ::protobuf::descriptor::FileDescriptorProto,
+> = ::protobuf::lazy::Lazy::INIT;
 
 
 fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
 fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
     ::protobuf::parse_from_bytes(file_descriptor_proto_data).unwrap()
     ::protobuf::parse_from_bytes(file_descriptor_proto_data).unwrap()
 }
 }
 
 
 pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
 pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
-    unsafe {
-        file_descriptor_proto_lazy.get(|| {
-            parse_descriptor_proto()
-        })
-    }
+    unsafe { file_descriptor_proto_lazy.get(|| parse_descriptor_proto()) }
 }
 }

File diff suppressed because it is too large
+ 316 - 204
protocol/src/playlist4meta.rs


File diff suppressed because it is too large
+ 345 - 217
protocol/src/playlist4ops.rs


+ 65 - 52
protocol/src/pubsub.rs

@@ -1,7 +1,7 @@
-// This file is generated by rust-protobuf 2.8.1. Do not edit
+// This file is generated by rust-protobuf 2.14.0. Do not edit
 // @generated
 // @generated
 
 
-// https://github.com/Manishearth/rust-clippy/issues/702
+// https://github.com/rust-lang/rust-clippy/issues/702
 #![allow(unknown_lints)]
 #![allow(unknown_lints)]
 #![allow(clippy::all)]
 #![allow(clippy::all)]
 
 
@@ -24,9 +24,9 @@ use protobuf::ProtobufEnum as ProtobufEnum_imported_for_functions;
 
 
 /// Generated files are compatible only with the same version
 /// Generated files are compatible only with the same version
 /// of protobuf runtime.
 /// of protobuf runtime.
-const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_8_1;
+// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_14_0;
 
 
-#[derive(PartialEq,Clone,Default)]
+#[derive(PartialEq, Clone, Default)]
 pub struct Subscription {
 pub struct Subscription {
     // message fields
     // message fields
     uri: ::protobuf::SingularField<::std::string::String>,
     uri: ::protobuf::SingularField<::std::string::String>,
@@ -50,7 +50,6 @@ impl Subscription {
 
 
     // optional string uri = 1;
     // optional string uri = 1;
 
 
-
     pub fn get_uri(&self) -> &str {
     pub fn get_uri(&self) -> &str {
         match self.uri.as_ref() {
         match self.uri.as_ref() {
             Some(v) => &v,
             Some(v) => &v,
@@ -81,12 +80,13 @@ impl Subscription {
 
 
     // Take field
     // Take field
     pub fn take_uri(&mut self) -> ::std::string::String {
     pub fn take_uri(&mut self) -> ::std::string::String {
-        self.uri.take().unwrap_or_else(|| ::std::string::String::new())
+        self.uri
+            .take()
+            .unwrap_or_else(|| ::std::string::String::new())
     }
     }
 
 
     // optional int32 expiry = 2;
     // optional int32 expiry = 2;
 
 
-
     pub fn get_expiry(&self) -> i32 {
     pub fn get_expiry(&self) -> i32 {
         self.expiry.unwrap_or(0)
         self.expiry.unwrap_or(0)
     }
     }
@@ -105,7 +105,6 @@ impl Subscription {
 
 
     // optional int32 status_code = 3;
     // optional int32 status_code = 3;
 
 
-
     pub fn get_status_code(&self) -> i32 {
     pub fn get_status_code(&self) -> i32 {
         self.status_code.unwrap_or(0)
         self.status_code.unwrap_or(0)
     }
     }
@@ -128,30 +127,42 @@ impl ::protobuf::Message for Subscription {
         true
         true
     }
     }
 
 
-    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+    fn merge_from(
+        &mut self,
+        is: &mut ::protobuf::CodedInputStream<'_>,
+    ) -> ::protobuf::ProtobufResult<()> {
         while !is.eof()? {
         while !is.eof()? {
             let (field_number, wire_type) = is.read_tag_unpack()?;
             let (field_number, wire_type) = is.read_tag_unpack()?;
             match field_number {
             match field_number {
                 1 => {
                 1 => {
                     ::protobuf::rt::read_singular_string_into(wire_type, is, &mut self.uri)?;
                     ::protobuf::rt::read_singular_string_into(wire_type, is, &mut self.uri)?;
-                },
+                }
                 2 => {
                 2 => {
                     if wire_type != ::protobuf::wire_format::WireTypeVarint {
                     if wire_type != ::protobuf::wire_format::WireTypeVarint {
-                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(
+                            wire_type,
+                        ));
                     }
                     }
                     let tmp = is.read_int32()?;
                     let tmp = is.read_int32()?;
                     self.expiry = ::std::option::Option::Some(tmp);
                     self.expiry = ::std::option::Option::Some(tmp);
-                },
+                }
                 3 => {
                 3 => {
                     if wire_type != ::protobuf::wire_format::WireTypeVarint {
                     if wire_type != ::protobuf::wire_format::WireTypeVarint {
-                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(
+                            wire_type,
+                        ));
                     }
                     }
                     let tmp = is.read_int32()?;
                     let tmp = is.read_int32()?;
                     self.status_code = ::std::option::Option::Some(tmp);
                     self.status_code = ::std::option::Option::Some(tmp);
-                },
+                }
                 _ => {
                 _ => {
-                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
-                },
+                    ::protobuf::rt::read_unknown_or_skip_group(
+                        field_number,
+                        wire_type,
+                        is,
+                        self.mut_unknown_fields(),
+                    )?;
+                }
             };
             };
         }
         }
         ::std::result::Result::Ok(())
         ::std::result::Result::Ok(())
@@ -175,7 +186,10 @@ impl ::protobuf::Message for Subscription {
         my_size
         my_size
     }
     }
 
 
-    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+    fn write_to_with_cached_sizes(
+        &self,
+        os: &mut ::protobuf::CodedOutputStream<'_>,
+    ) -> ::protobuf::ProtobufResult<()> {
         if let Some(ref v) = self.uri.as_ref() {
         if let Some(ref v) = self.uri.as_ref() {
             os.write_string(1, &v)?;
             os.write_string(1, &v)?;
         }
         }
@@ -220,45 +234,49 @@ impl ::protobuf::Message for Subscription {
     }
     }
 
 
     fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
     fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
-        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy {
-            lock: ::protobuf::lazy::ONCE_INIT,
-            ptr: 0 as *const ::protobuf::reflect::MessageDescriptor,
-        };
+        static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> =
+            ::protobuf::lazy::Lazy::INIT;
         unsafe {
         unsafe {
             descriptor.get(|| {
             descriptor.get(|| {
                 let mut fields = ::std::vec::Vec::new();
                 let mut fields = ::std::vec::Vec::new();
-                fields.push(::protobuf::reflect::accessor::make_singular_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
-                    "uri",
-                    |m: &Subscription| { &m.uri },
-                    |m: &mut Subscription| { &mut m.uri },
-                ));
-                fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>(
+                fields.push(
+                    ::protobuf::reflect::accessor::make_singular_field_accessor::<
+                        _,
+                        ::protobuf::types::ProtobufTypeString,
+                    >(
+                        "uri",
+                        |m: &Subscription| &m.uri,
+                        |m: &mut Subscription| &mut m.uri,
+                    ),
+                );
+                fields.push(::protobuf::reflect::accessor::make_option_accessor::<
+                    _,
+                    ::protobuf::types::ProtobufTypeInt32,
+                >(
                     "expiry",
                     "expiry",
-                    |m: &Subscription| { &m.expiry },
-                    |m: &mut Subscription| { &mut m.expiry },
+                    |m: &Subscription| &m.expiry,
+                    |m: &mut Subscription| &mut m.expiry,
                 ));
                 ));
-                fields.push(::protobuf::reflect::accessor::make_option_accessor::<_, ::protobuf::types::ProtobufTypeInt32>(
+                fields.push(::protobuf::reflect::accessor::make_option_accessor::<
+                    _,
+                    ::protobuf::types::ProtobufTypeInt32,
+                >(
                     "status_code",
                     "status_code",
-                    |m: &Subscription| { &m.status_code },
-                    |m: &mut Subscription| { &mut m.status_code },
+                    |m: &Subscription| &m.status_code,
+                    |m: &mut Subscription| &mut m.status_code,
                 ));
                 ));
-                ::protobuf::reflect::MessageDescriptor::new::<Subscription>(
+                ::protobuf::reflect::MessageDescriptor::new_pb_name::<Subscription>(
                     "Subscription",
                     "Subscription",
                     fields,
                     fields,
-                    file_descriptor_proto()
+                    file_descriptor_proto(),
                 )
                 )
             })
             })
         }
         }
     }
     }
 
 
     fn default_instance() -> &'static Subscription {
     fn default_instance() -> &'static Subscription {
-        static mut instance: ::protobuf::lazy::Lazy<Subscription> = ::protobuf::lazy::Lazy {
-            lock: ::protobuf::lazy::ONCE_INIT,
-            ptr: 0 as *const Subscription,
-        };
-        unsafe {
-            instance.get(Subscription::new)
-        }
+        static mut instance: ::protobuf::lazy::Lazy<Subscription> = ::protobuf::lazy::Lazy::INIT;
+        unsafe { instance.get(Subscription::new) }
     }
     }
 }
 }
 
 
@@ -278,8 +296,8 @@ impl ::std::fmt::Debug for Subscription {
 }
 }
 
 
 impl ::protobuf::reflect::ProtobufValue for Subscription {
 impl ::protobuf::reflect::ProtobufValue for Subscription {
-    fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
-        ::protobuf::reflect::ProtobufValueRef::Message(self)
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
     }
     }
 }
 }
 
 
@@ -289,19 +307,14 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     us_code\x18\x03\x20\x01(\x05B\0:\0B\0b\x06proto2\
     us_code\x18\x03\x20\x01(\x05B\0:\0B\0b\x06proto2\
 ";
 ";
 
 
-static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {
-    lock: ::protobuf::lazy::ONCE_INIT,
-    ptr: 0 as *const ::protobuf::descriptor::FileDescriptorProto,
-};
+static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<
+    ::protobuf::descriptor::FileDescriptorProto,
+> = ::protobuf::lazy::Lazy::INIT;
 
 
 fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
 fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
     ::protobuf::parse_from_bytes(file_descriptor_proto_data).unwrap()
     ::protobuf::parse_from_bytes(file_descriptor_proto_data).unwrap()
 }
 }
 
 
 pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
 pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
-    unsafe {
-        file_descriptor_proto_lazy.get(|| {
-            parse_descriptor_proto()
-        })
-    }
+    unsafe { file_descriptor_proto_lazy.get(|| parse_descriptor_proto()) }
 }
 }

File diff suppressed because it is too large
+ 403 - 245
protocol/src/spirc.rs


+ 2 - 1
publish.sh

@@ -47,7 +47,7 @@ function publishCrates {
     crate_path="$WORKINGDIR/$CRATE"
     crate_path="$WORKINGDIR/$CRATE"
     crate_path=${crate_path//\/\///}
     crate_path=${crate_path//\/\///}
     cd $crate_path
     cd $crate_path
-
+    # Also need to update Cargo.lock in root directory
     crate_name=`echo $( awk -v FS="name = " 'NF>1{print $2; exit}' Cargo.toml )`
     crate_name=`echo $( awk -v FS="name = " 'NF>1{print $2; exit}' Cargo.toml )`
     echo "Publishing $crate_name to crates.io"
     echo "Publishing $crate_name to crates.io"
     if [ "$CRATE" == "protocol" ]
     if [ "$CRATE" == "protocol" ]
@@ -58,6 +58,7 @@ function publishCrates {
       cargo publish
       cargo publish
     fi
     fi
     echo "Successfully published $crate_name to crates.io"
     echo "Successfully published $crate_name to crates.io"
+    # Should sleep here for 30 seconds to allow Crates.io time to push updated package to edge servers.
   done
   done
 }
 }
 
 

+ 15 - 1
src/main.rs

@@ -27,7 +27,7 @@ use librespot::playback::mixer::{self, Mixer, MixerConfig};
 use librespot::playback::player::{Player, PlayerEvent};
 use librespot::playback::player::{Player, PlayerEvent};
 
 
 mod player_event_handler;
 mod player_event_handler;
-use crate::player_event_handler::run_program_on_events;
+use crate::player_event_handler::{emit_sink_event, run_program_on_events};
 
 
 fn device_id(name: &str) -> String {
 fn device_id(name: &str) -> String {
     hex::encode(Sha1::digest(name.as_bytes()))
     hex::encode(Sha1::digest(name.as_bytes()))
@@ -87,6 +87,7 @@ struct Setup {
     enable_discovery: bool,
     enable_discovery: bool,
     zeroconf_port: u16,
     zeroconf_port: u16,
     player_event_program: Option<String>,
     player_event_program: Option<String>,
+    emit_sink_events: bool,
 }
 }
 
 
 fn setup(args: &[String]) -> Setup {
 fn setup(args: &[String]) -> Setup {
@@ -111,6 +112,7 @@ fn setup(args: &[String]) -> Setup {
             "Run PROGRAM when playback is about to begin.",
             "Run PROGRAM when playback is about to begin.",
             "PROGRAM",
             "PROGRAM",
         )
         )
+        .optflag("", "emit-sink-events", "Run program set by --onevent before sink is opened and after it is closed.")
         .optflag("v", "verbose", "Enable verbose output")
         .optflag("v", "verbose", "Enable verbose output")
         .optopt("u", "username", "Username to sign in with", "USERNAME")
         .optopt("u", "username", "Username to sign in with", "USERNAME")
         .optopt("p", "password", "Password", "PASSWORD")
         .optopt("p", "password", "Password", "PASSWORD")
@@ -359,6 +361,7 @@ fn setup(args: &[String]) -> Setup {
         mixer: mixer,
         mixer: mixer,
         mixer_config: mixer_config,
         mixer_config: mixer_config,
         player_event_program: matches.opt_str("onevent"),
         player_event_program: matches.opt_str("onevent"),
+        emit_sink_events: matches.opt_present("emit-sink-events"),
     }
     }
 }
 }
 
 
@@ -386,6 +389,7 @@ struct Main {
 
 
     player_event_channel: Option<UnboundedReceiver<PlayerEvent>>,
     player_event_channel: Option<UnboundedReceiver<PlayerEvent>>,
     player_event_program: Option<String>,
     player_event_program: Option<String>,
+    emit_sink_events: bool,
 }
 }
 
 
 impl Main {
 impl Main {
@@ -412,6 +416,7 @@ impl Main {
 
 
             player_event_channel: None,
             player_event_channel: None,
             player_event_program: setup.player_event_program,
             player_event_program: setup.player_event_program,
+            emit_sink_events: setup.emit_sink_events,
         };
         };
 
 
         if setup.enable_discovery {
         if setup.enable_discovery {
@@ -481,6 +486,15 @@ impl Future for Main {
                             (backend)(device)
                             (backend)(device)
                         });
                         });
 
 
+                    if self.emit_sink_events {
+                        if let Some(player_event_program) = &self.player_event_program {
+                            let player_event_program = player_event_program.clone();
+                            player.set_sink_event_callback(Some(Box::new(move |sink_status| {
+                                emit_sink_event(sink_status, &player_event_program)
+                            })));
+                        }
+                    }
+
                     let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer);
                     let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer);
                     self.spirc = Some(spirc);
                     self.spirc = Some(spirc);
                     self.spirc_task = Some(spirc_task);
                     self.spirc_task = Some(spirc_task);

+ 16 - 0
src/player_event_handler.rs

@@ -5,6 +5,9 @@ use std::io;
 use std::process::Command;
 use std::process::Command;
 use tokio_process::{Child, CommandExt};
 use tokio_process::{Child, CommandExt};
 
 
+use futures::Future;
+use librespot::playback::player::SinkStatus;
+
 fn run_program(program: &str, env_vars: HashMap<&str, String>) -> io::Result<Child> {
 fn run_program(program: &str, env_vars: HashMap<&str, String>) -> io::Result<Child> {
     let mut v: Vec<&str> = program.split_whitespace().collect();
     let mut v: Vec<&str> = program.split_whitespace().collect();
     info!("Running {:?} with environment variables {:?}", v, env_vars);
     info!("Running {:?} with environment variables {:?}", v, env_vars);
@@ -63,3 +66,16 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<io::Re
     }
     }
     Some(run_program(onevent, env_vars))
     Some(run_program(onevent, env_vars))
 }
 }
+
+pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) {
+    let mut env_vars = HashMap::new();
+    env_vars.insert("PLAYER_EVENT", "sink".to_string());
+    let sink_status = match sink_status {
+        SinkStatus::Running => "running",
+        SinkStatus::TemporarilyClosed => "temporarily_closed",
+        SinkStatus::Closed => "closed",
+    };
+    env_vars.insert("SINK_STATUS", sink_status.to_string());
+
+    let _ = run_program(onevent, env_vars).and_then(|child| child.wait());
+}

Some files were not shown because too many files changed in this diff