From 14c177905608f98eb528fae3a66debc3592c79d3 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Wed, 26 May 2021 20:45:22 +0200 Subject: [PATCH 01/95] Adjust arg types of `Credentials::with_blob` ... to avoid redundant utf-8 checking --- core/src/authentication.rs | 16 +++++++++++----- discovery/src/server.rs | 5 ++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/src/authentication.rs b/core/src/authentication.rs index db787bbe..3c188ecf 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -42,7 +42,11 @@ impl Credentials { } } - pub fn with_blob(username: String, encrypted_blob: &str, device_id: &str) -> Credentials { + pub fn with_blob( + username: impl Into, + encrypted_blob: impl AsRef<[u8]>, + device_id: impl AsRef<[u8]>, + ) -> Credentials { fn read_u8(stream: &mut R) -> io::Result { let mut data = [0u8]; stream.read_exact(&mut data)?; @@ -67,7 +71,9 @@ impl Credentials { Ok(data) } - let secret = Sha1::digest(device_id.as_bytes()); + let username = username.into(); + + let secret = Sha1::digest(device_id.as_ref()); let key = { let mut key = [0u8; 24]; @@ -88,9 +94,9 @@ impl Credentials { let mut data = base64::decode(encrypted_blob).unwrap(); let cipher = Aes192::new(GenericArray::from_slice(&key)); let block_size = ::BlockSize::to_usize(); + assert_eq!(data.len() % block_size, 0); - // replace to chunks_exact_mut with MSRV bump to 1.31 - for chunk in data.chunks_mut(block_size) { + for chunk in data.chunks_exact_mut(block_size) { cipher.decrypt_block(GenericArray::from_mut_slice(chunk)); } @@ -102,7 +108,7 @@ impl Credentials { data }; - let mut cursor = io::Cursor::new(&blob); + let mut cursor = io::Cursor::new(blob.as_slice()); read_u8(&mut cursor).unwrap(); read_bytes(&mut cursor).unwrap(); read_u8(&mut cursor).unwrap(); diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 53b849f7..57f5bf46 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -129,11 +129,10 @@ impl RequestHandler { GenericArray::from_slice(iv), ); cipher.apply_keystream(&mut data); - String::from_utf8(data).unwrap() + data }; - let credentials = - Credentials::with_blob(username.to_string(), &decrypted, &self.config.device_id); + let credentials = Credentials::with_blob(username, &decrypted, &self.config.device_id); self.tx.send(credentials).unwrap(); From bb2477831ba1299f60bd4ca9957391fc72c62015 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Fri, 25 Jun 2021 14:16:58 -0500 Subject: [PATCH 02/95] Don't explicitly set the number of periods Doing so on configs that have less than the 4 periods we were asking for caused a crash. Instead ask for a buffer time of 500ms. --- playback/src/audio_backend/alsa.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 7101f96d..a9a593a3 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -12,9 +12,9 @@ use std::process::exit; use std::time::Duration; use thiserror::Error; -// 125 ms Period time * 4 periods = 0.5 sec buffer. -const PERIOD_TIME: Duration = Duration::from_millis(125); -const NUM_PERIODS: u32 = 4; +// 0.5 sec buffer. +const PERIOD_TIME: Duration = Duration::from_millis(100); +const BUFFER_TIME: Duration = Duration::from_millis(500); #[derive(Debug, Error)] enum AlsaError { @@ -131,8 +131,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Alsa err: e, })?; - // Deal strictly in time and periods. - hwp.set_periods(NUM_PERIODS, ValueOr::Nearest) + hwp.set_buffer_time_near(BUFFER_TIME.as_micros() as u32, ValueOr::Nearest) .map_err(AlsaError::HwParams)?; hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest) From 751ccf63bb7d3928ffad0b28ec9a65264ccc39a8 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Wed, 30 Jun 2021 09:54:02 +0200 Subject: [PATCH 03/95] Make `convert` and `decoder` public (#814) --- CHANGELOG.md | 1 + playback/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb63541..86e5763e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `alsamixer`: query card dB range for the `log` volume control unless specified otherwise - [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise - [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking) +- [playback] `player`: make `convert` and `decoder` public so you can implement your own `Sink` ### Deprecated - [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate diff --git a/playback/src/lib.rs b/playback/src/lib.rs index 689b8470..e39dfc7c 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -7,8 +7,8 @@ use librespot_metadata as metadata; pub mod audio_backend; pub mod config; -mod convert; -mod decoder; +pub mod convert; +pub mod decoder; pub mod dither; pub mod mixer; pub mod player; From 9ff33980d6a74cc264795a1471d0497a249bcba3 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Wed, 30 Jun 2021 14:14:23 -0500 Subject: [PATCH 04/95] Better errors in PulseAudio backend (#801) * More meaningful error messages * Use F32 if a user requests F64 (F64 is not supported by PulseAudio) * Move all code that can fail to `start` where errors can be returned to prevent panics * Use drain in `stop` --- CHANGELOG.md | 2 +- playback/Cargo.toml | 2 +- playback/src/audio_backend/pulseaudio.rs | 130 ++++++++++++++--------- 3 files changed, 82 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86e5763e..2ecd12f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `alsamixer`: make `--volume-ctrl {linear|log}` work as expected - [playback] `alsa`, `gstreamer`, `pulseaudio`: always output in native endianness - [playback] `alsa`: revert buffer size to ~500 ms -- [playback] `alsa`, `pipe`: better error handling +- [playback] `alsa`, `pipe`, `pulseaudio`: better error handling ## [0.2.0] - 2021-05-04 diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 0bed793c..8211f2bd 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -53,7 +53,7 @@ rand_distr = "0.4" [features] alsa-backend = ["alsa", "thiserror"] portaudio-backend = ["portaudio-rs"] -pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] +pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding", "thiserror"] jackaudio-backend = ["jack"] rodio-backend = ["rodio", "cpal", "thiserror"] rodiojack-backend = ["rodio", "cpal/jack", "thiserror"] diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index e36941ea..4ef8317a 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -3,48 +3,49 @@ use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; -use libpulse_binding::{self as pulse, stream::Direction}; +use libpulse_binding::{self as pulse, error::PAErr, stream::Direction}; use libpulse_simple_binding::Simple; use std::io; +use thiserror::Error; const APP_NAME: &str = "librespot"; const STREAM_NAME: &str = "Spotify endpoint"; +#[derive(Debug, Error)] +enum PulseError { + #[error("Error starting PulseAudioSink, invalid PulseAudio sample spec")] + InvalidSampleSpec, + #[error("Error starting PulseAudioSink, could not connect to PulseAudio server, {0}")] + ConnectionRefused(PAErr), + #[error("Error stopping PulseAudioSink, failed to drain PulseAudio server buffer, {0}")] + DrainFailure(PAErr), + #[error("Error in PulseAudioSink, Not connected to PulseAudio server")] + NotConnected, + #[error("Error writing from PulseAudioSink to PulseAudio server, {0}")] + OnWrite(PAErr), +} + pub struct PulseAudioSink { s: Option, - ss: pulse::sample::Spec, device: Option, format: AudioFormat, } impl Open for PulseAudioSink { fn open(device: Option, format: AudioFormat) -> Self { - info!("Using PulseAudio sink with format: {:?}", format); + let mut actual_format = format; - // PulseAudio calls S24 and S24_3 different from the rest of the world - let pulse_format = match format { - AudioFormat::F32 => pulse::sample::Format::FLOAT32NE, - AudioFormat::S32 => pulse::sample::Format::S32NE, - AudioFormat::S24 => pulse::sample::Format::S24_32NE, - AudioFormat::S24_3 => pulse::sample::Format::S24NE, - AudioFormat::S16 => pulse::sample::Format::S16NE, - _ => { - unimplemented!("PulseAudio currently does not support {:?} output", format) - } - }; + if actual_format == AudioFormat::F64 { + warn!("PulseAudio currently does not support F64 output"); + actual_format = AudioFormat::F32; + } - let ss = pulse::sample::Spec { - format: pulse_format, - channels: NUM_CHANNELS, - rate: SAMPLE_RATE, - }; - debug_assert!(ss.is_valid()); + info!("Using PulseAudioSink with format: {:?}", actual_format); Self { s: None, - ss, device, - format, + format: actual_format, } } } @@ -55,30 +56,64 @@ impl Sink for PulseAudioSink { return Ok(()); } - let device = self.device.as_deref(); + // PulseAudio calls S24 and S24_3 different from the rest of the world + let pulse_format = match self.format { + AudioFormat::F32 => pulse::sample::Format::FLOAT32NE, + AudioFormat::S32 => pulse::sample::Format::S32NE, + AudioFormat::S24 => pulse::sample::Format::S24_32NE, + AudioFormat::S24_3 => pulse::sample::Format::S24NE, + AudioFormat::S16 => pulse::sample::Format::S16NE, + _ => unreachable!(), + }; + + let ss = pulse::sample::Spec { + format: pulse_format, + channels: NUM_CHANNELS, + rate: SAMPLE_RATE, + }; + + if !ss.is_valid() { + return Err(io::Error::new( + io::ErrorKind::Other, + PulseError::InvalidSampleSpec, + )); + } + let result = Simple::new( - None, // Use the default server. - APP_NAME, // Our application's name. - Direction::Playback, // Direction. - device, // Our device (sink) name. - STREAM_NAME, // Description of our stream. - &self.ss, // Our sample format. - None, // Use default channel map. - None, // Use default buffering attributes. + None, // Use the default server. + APP_NAME, // Our application's name. + Direction::Playback, // Direction. + self.device.as_deref(), // Our device (sink) name. + STREAM_NAME, // Description of our stream. + &ss, // Our sample format. + None, // Use default channel map. + None, // Use default buffering attributes. ); + match result { Ok(s) => { self.s = Some(s); - Ok(()) } - Err(e) => Err(io::Error::new( - io::ErrorKind::ConnectionRefused, - e.to_string().unwrap(), - )), + Err(e) => { + return Err(io::Error::new( + io::ErrorKind::ConnectionRefused, + PulseError::ConnectionRefused(e), + )); + } } + + Ok(()) } fn stop(&mut self) -> io::Result<()> { + let s = self + .s + .as_mut() + .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, PulseError::NotConnected))?; + + s.drain() + .map_err(|e| io::Error::new(io::ErrorKind::Other, PulseError::DrainFailure(e)))?; + self.s = None; Ok(()) } @@ -88,20 +123,15 @@ impl Sink for PulseAudioSink { impl SinkAsBytes for PulseAudioSink { fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { - if let Some(s) = &self.s { - match s.write(data) { - Ok(_) => Ok(()), - Err(e) => Err(io::Error::new( - io::ErrorKind::BrokenPipe, - e.to_string().unwrap(), - )), - } - } else { - Err(io::Error::new( - io::ErrorKind::NotConnected, - "Not connected to PulseAudio", - )) - } + let s = self + .s + .as_mut() + .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, PulseError::NotConnected))?; + + s.write(data) + .map_err(|e| io::Error::new(io::ErrorKind::Other, PulseError::OnWrite(e)))?; + + Ok(()) } } From b519a4a47d5ee3ecb5b3d70b702bdb244ada7388 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 30 Jun 2021 21:39:55 +0200 Subject: [PATCH 05/95] Update crates (#817) --- Cargo.lock | 167 +++++++++++++++++++---------------------------------- 1 file changed, 61 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e7bd92dc..64695723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aes" version = "0.6.0" @@ -76,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" +checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" [[package]] name = "async-trait" @@ -150,9 +152,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.6.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" [[package]] name = "byteorder" @@ -248,9 +250,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.5.2" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4369b5e4c0cddf64ad8981c0111e7df4f7078f4d6ba98fb31f2e17c4c57b7e" +checksum = "a2d47c1b11006b87e492b53b313bb699ce60e16613c4dddaa91f8f7c220ab2fa" dependencies = [ "bytes", "memchr", @@ -309,9 +311,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" dependencies = [ "libc", ] @@ -408,9 +410,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "env_logger" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" dependencies = [ "atty", "humantime", @@ -721,9 +723,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "headers" @@ -752,18 +754,18 @@ dependencies = [ [[package]] name = "heck" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] @@ -837,9 +839,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.8" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3f71a7eea53a3f8257a7b4795373ff886397178cd634430ea94e12d7fe4fe34" +checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83" dependencies = [ "bytes", "futures-channel", @@ -850,7 +852,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project", + "pin-project-lite", "socket2", "tokio", "tower-service", @@ -913,9 +915,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", "hashbrown", @@ -1043,9 +1045,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" [[package]] name = "libloading" @@ -1368,9 +1370,9 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" dependencies = [ "libc", "log", @@ -1599,9 +1601,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "opaque-debug" @@ -1662,40 +1664,11 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "pin-project" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pin-project-lite" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pin-utils" @@ -1808,24 +1781,24 @@ dependencies = [ [[package]] name = "protobuf" -version = "2.23.0" +version = "2.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45604fc7a88158e7d514d8e22e14ac746081e7a70d7690074dd0029ee37458d6" +checksum = "db50e77ae196458ccd3dc58a31ea1a90b0698ab1b7928d89f644c25d72070267" [[package]] name = "protobuf-codegen" -version = "2.23.0" +version = "2.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb87f342b585958c1c086313dbc468dcac3edf5e90362111c26d7a58127ac095" +checksum = "09321cef9bee9ddd36884f97b7f7cc92a586cdc74205c4b3aeba65b5fc9c6f90" dependencies = [ "protobuf", ] [[package]] name = "protobuf-codegen-pure" -version = "2.23.0" +version = "2.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca6e0e2f898f7856a6328650abc9b2df71b7c1a5f39be0800d19051ad0214b2" +checksum = "1afb68a6d768571da3db86ce55f0f62966e0fc25eaf96acd070ea548a91b0d23" dependencies = [ "protobuf", "protobuf-codegen", @@ -1842,9 +1815,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", "rand_chacha", @@ -1854,9 +1827,9 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -1864,18 +1837,18 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] name = "rand_distr" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9e8f32ad24fb80d07d2323a9a2ce8b30d68a62b8cb4df88119ff49a698f038" +checksum = "051b398806e42b9cd04ad9ec8f81e355d0a382c543ac6672c62f5a5b452ef142" dependencies = [ "num-traits", "rand", @@ -1883,18 +1856,18 @@ dependencies = [ [[package]] name = "rand_hc" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ "rand_core", ] [[package]] name = "redox_syscall" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" dependencies = [ "bitflags", ] @@ -1952,9 +1925,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ "semver", ] @@ -2005,21 +1978,9 @@ dependencies = [ [[package]] name = "semver" -version = "0.11.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] +checksum = "5f3aac57ee7f3272d8395c6e4f502f434f0e289fcd62876f70daa008c20dcabe" [[package]] name = "serde" @@ -2088,9 +2049,9 @@ checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" [[package]] name = "signal-hook-registry" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ "libc", ] @@ -2168,9 +2129,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" [[package]] name = "syn" -version = "1.0.72" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" dependencies = [ "proc-macro2", "quote", @@ -2274,9 +2235,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37" +checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2" dependencies = [ "autocfg", "bytes", @@ -2374,12 +2335,6 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - [[package]] name = "unicode-bidi" version = "0.3.5" @@ -2391,9 +2346,9 @@ dependencies = [ [[package]] name = "unicode-normalization" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" dependencies = [ "tinyvec", ] From 68bec41e08564187e27d8c132b9b7b8274579819 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Tue, 6 Jul 2021 01:37:29 -0500 Subject: [PATCH 06/95] Improve Alsa backend buffer (#811) * Reuse the buffer for the life of the Alsa sink * Don't depend on capacity being exact when sizing the buffer * Always give the PCM a period's worth of audio even when draining the buffer * Refactoring and code cleanup --- playback/src/audio_backend/alsa.rs | 79 ++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index a9a593a3..7b5987a3 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -152,10 +152,15 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Alsa pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; + trace!("Frames per Buffer: {:?}", frames_per_buffer); + trace!("Frames per Period: {:?}", frames_per_period); + // Let ALSA do the math for us. pcm.frames_to_bytes(frames_per_period) as usize }; + trace!("Period Buffer size in bytes: {:?}", bytes_per_period); + Ok((pcm, bytes_per_period)) } @@ -193,7 +198,22 @@ impl Sink for AlsaSink { match open_device(&self.device, self.format) { Ok((pcm, bytes_per_period)) => { self.pcm = Some(pcm); - self.period_buffer = Vec::with_capacity(bytes_per_period); + // If the capacity is greater than we want shrink it + // to it's current len (which should be zero) before + // setting the capacity with reserve_exact. + if self.period_buffer.capacity() > bytes_per_period { + self.period_buffer.shrink_to_fit(); + } + // This does nothing if the capacity is already sufficient. + // Len should always be zero, but for the sake of being thorough... + self.period_buffer + .reserve_exact(bytes_per_period - self.period_buffer.len()); + + // Should always match the "Period Buffer size in bytes: " trace! message. + trace!( + "Period Buffer capacity: {:?}", + self.period_buffer.capacity() + ); } Err(e) => { return Err(io::Error::new(io::ErrorKind::Other, e)); @@ -205,20 +225,22 @@ impl Sink for AlsaSink { } fn stop(&mut self) -> io::Result<()> { - { - // Write any leftover data in the period buffer - // before draining the actual buffer - self.write_bytes(&[])?; - let pcm = self.pcm.as_mut().ok_or_else(|| { - io::Error::new(io::ErrorKind::Other, "Error stopping AlsaSink, PCM is None") - })?; - pcm.drain().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("Error stopping AlsaSink {}", e), - ) - })? - } + // Zero fill the remainder of the period buffer and + // write any leftover data before draining the actual PCM buffer. + self.period_buffer.resize(self.period_buffer.capacity(), 0); + self.write_buf()?; + + let pcm = self.pcm.as_mut().ok_or_else(|| { + io::Error::new(io::ErrorKind::Other, "Error stopping AlsaSink, PCM is None") + })?; + + pcm.drain().map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("Error stopping AlsaSink {}", e), + ) + })?; + self.pcm = None; Ok(()) } @@ -228,22 +250,24 @@ impl Sink for AlsaSink { impl SinkAsBytes for AlsaSink { fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { - let mut processed_data = 0; - while processed_data < data.len() { - let data_to_buffer = min( - self.period_buffer.capacity() - self.period_buffer.len(), - data.len() - processed_data, - ); + let mut start_index = 0; + let data_len = data.len(); + let capacity = self.period_buffer.capacity(); + loop { + let data_left = data_len - start_index; + let space_left = capacity - self.period_buffer.len(); + let data_to_buffer = min(data_left, space_left); + let end_index = start_index + data_to_buffer; self.period_buffer - .extend_from_slice(&data[processed_data..processed_data + data_to_buffer]); - processed_data += data_to_buffer; - if self.period_buffer.len() == self.period_buffer.capacity() { + .extend_from_slice(&data[start_index..end_index]); + if self.period_buffer.len() == capacity { self.write_buf()?; - self.period_buffer.clear(); } + if end_index == data_len { + break Ok(()); + } + start_index = end_index; } - - Ok(()) } } @@ -276,6 +300,7 @@ impl AlsaSink { })? } + self.period_buffer.clear(); Ok(()) } } From 4c00b19c29d1c29a528e332113f20a0ce9cdcb34 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Jul 2021 20:12:44 +0200 Subject: [PATCH 07/95] Fix Alsa mixer --- src/main.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index a3687aaa..aa04d0d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -205,6 +205,7 @@ fn get_setup(args: &[String]) -> Setup { const FORMAT: &str = "format"; const HELP: &str = "h"; const INITIAL_VOLUME: &str = "initial-volume"; + const MIXER_TYPE: &str = "mixer"; const MIXER_CARD: &str = "mixer-card"; const MIXER_INDEX: &str = "mixer-index"; const MIXER_NAME: &str = "mixer-name"; @@ -295,7 +296,7 @@ fn get_setup(args: &[String]) -> Setup { "Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", "DITHER", ) - .optopt("", "mixer", "Mixer to use {alsa|softvol}.", "MIXER") + .optopt("", MIXER_TYPE, "Mixer to use {alsa|softvol}.", "MIXER") .optopt( "m", MIXER_NAME, @@ -454,8 +455,8 @@ fn get_setup(args: &[String]) -> Setup { exit(0); } - let mixer_name = matches.opt_str(MIXER_NAME); - let mixer = mixer::find(mixer_name.as_deref()).expect("Invalid mixer"); + let mixer_type = matches.opt_str(MIXER_TYPE); + let mixer = mixer::find(mixer_type.as_deref()).expect("Invalid mixer"); let mixer_config = { let card = matches.opt_str(MIXER_CARD).unwrap_or_else(|| { @@ -475,7 +476,7 @@ fn get_setup(args: &[String]) -> Setup { let mut volume_range = matches .opt_str(VOLUME_RANGE) .map(|range| range.parse::().unwrap()) - .unwrap_or_else(|| match mixer_name.as_deref() { + .unwrap_or_else(|| match mixer_type.as_deref() { #[cfg(feature = "alsa-backend")] Some(AlsaMixer::NAME) => 0.0, // let Alsa query the control _ => VolumeCtrl::DEFAULT_DB_RANGE, @@ -563,7 +564,7 @@ fn get_setup(args: &[String]) -> Setup { } (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 }) - .or_else(|| match mixer_name.as_deref() { + .or_else(|| match mixer_type.as_deref() { #[cfg(feature = "alsa-backend")] Some(AlsaMixer::NAME) => None, _ => cache.as_ref().and_then(Cache::volume), From 2541f123bcbb69454de901f48785988df61a7d68 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Jul 2021 21:02:48 +0200 Subject: [PATCH 08/95] Update documentation --- COMPILING.md | 25 +++++++++++-------------- CONTRIBUTING.md | 21 ++++++++++++++------- README.md | 12 +++++++----- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/COMPILING.md b/COMPILING.md index 8748cd0c..39ae20cc 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -5,20 +5,15 @@ In order to compile librespot, you will first need to set up a suitable Rust build environment, with the necessary dependencies installed. You will need to have a C compiler, Rust, and the development libraries for the audio backend(s) you want installed. These instructions will walk you through setting up a simple build environment. ### Install Rust -The easiest, and recommended way to get Rust is to use [rustup](https://rustup.rs). On Unix/MacOS You can install `rustup` with this command: - -```bash -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. +The easiest, and recommended way to get Rust is to use [rustup](https://rustup.rs). Once that’s installed, Rust's standard tools should be set up and ready to use. *Note: The current minimum required Rust version at the time of writing is 1.48, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* #### 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) and [`clippy`](https://github.com/rust-lang/rust-clippy), which are installed by default with `rustup` these days, else they can be installed manually with: ```bash rustup component add rustfmt +rustup component add clippy ``` Using `rustfmt` is not optional, as our CI checks against this repo's rules. @@ -43,12 +38,13 @@ Depending on the chosen backend, specific development libraries are required. |--------------------|------------------------------|-----------------------------------|-------------| |Rodio (default) | `libasound2-dev` | `alsa-lib-devel` | | |ALSA | `libasound2-dev, pkg-config` | `alsa-lib-devel` | | +|GStreamer | `gstreamer1.0-plugins-base libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good libgstreamer-plugins-good1.0-dev` | `gstreamer1 gstreamer1-devel gstreamer1-plugins-base-devel gstreamer1-plugins-good` | `gstreamer gst-devtools gst-plugins-base gst-plugins-good` | |PortAudio | `portaudio19-dev` | `portaudio-devel` | `portaudio` | |PulseAudio | `libpulse-dev` | `pulseaudio-libs-devel` | | -|JACK | `libjack-dev` | `jack-audio-connection-kit-devel` | | -|JACK over Rodio | `libjack-dev` | `jack-audio-connection-kit-devel` | - | -|SDL | `libsdl2-dev` | `SDL2-devel` | | -|Pipe | - | - | - | +|JACK | `libjack-dev` | `jack-audio-connection-kit-devel` | `jack` | +|JACK over Rodio | `libjack-dev` | `jack-audio-connection-kit-devel` | `jack` | +|SDL | `libsdl2-dev` | `SDL2-devel` | `sdl2` | +|Pipe & subprocess | - | - | - | ###### For example, to build an ALSA based backend, you would need to run the following to install the required dependencies: @@ -68,7 +64,6 @@ The recommended method is to first fork the repo, so that you have a copy that y ```bash git clone git@github.com:YOURUSERNAME/librespot.git -cd librespot ``` ## Compiling & Running @@ -109,7 +104,9 @@ cargo build --no-default-features --features "alsa-backend" Assuming you just compiled a ```debug``` build, you can run librespot with the following command: ```bash -./target/debug/librespot -n Librespot +./target/debug/librespot ``` There are various runtime options, documented in the wiki, and visible by running librespot with the ```-h``` argument. + +Note that debug builds may cause buffer underruns and choppy audio when dithering is enabled (which it is by default). You can disable dithering with ```--dither none```. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3395529c..907a7c04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,10 +8,12 @@ If you have encountered a bug, please report it, as we rely on user reports to f Please also make sure that your issues are helpful. To ensure that your issue is helpful, please read over this brief checklist to avoid the more common pitfalls: - - Please take a moment to search/read previous similar issues to ensure you aren’t posting a duplicate. Duplicates will be closed immediately. - - Please include a clear description of what the issue is. Issues with descriptions such as ‘It hangs after 40 minutes’ will be closed immediately. - - Please include, where possible, steps to reproduce the bug, along with any other material that is related to the bug. For example, if librespot consistently crashes when you try to play a song, please include the Spotify URI of that song. This can be immensely helpful in quickly pinpointing and resolving issues. - - Lastly, and perhaps most importantly, please include a backtrace where possible. Recent versions of librespot should produce these automatically when it crashes, and print them to the console, but in some cases, you may need to run ‘export RUST_BACKTRACE=full’ before running librespot to enable backtraces. +- Please take a moment to search/read previous similar issues to ensure you aren’t posting a duplicate. Duplicates will be closed immediately. +- Please include a clear description of what the issue is. Issues with descriptions such as ‘It hangs after 40 minutes’ will be closed immediately. +- Please include, where possible, steps to reproduce the bug, along with any other material that is related to the bug. For example, if librespot consistently crashes when you try to play a song, please include the Spotify URI of that song. This can be immensely helpful in quickly pinpointing and resolving issues. +- Please be alert and respond to questions asked by any project members. Stale issues will be closed. +- When your issue concerns audio playback, please first make sure that your audio system is set up correctly and can play audio from other applications. This project aims to provide correct audio backends, not to provide Linux support to end users. +- Lastly, and perhaps most importantly, please include a backtrace where possible. Recent versions of librespot should produce these automatically when it crashes, and print them to the console, but in some cases, you may need to run ‘export RUST_BACKTRACE=full’ before running librespot to enable backtraces. ## Contributing Code @@ -33,16 +35,21 @@ Unless your changes are negligible, please add an entry in the "Unreleased" sect Make sure that the code is correctly formatted by running: ```bash -cargo +stable fmt --all +cargo fmt --all ``` -This command runs the previously installed stable version of ```rustfmt```, a code formatting tool that will automatically correct any formatting that you have used that does not conform with the librespot code style. Once that command has run, you will need to rebuild the project: +This command runs ```rustfmt```, a code formatting tool that will automatically correct any formatting that you have used that does not conform with the librespot code style. Once that command has run, you will need to rebuild the project: ```bash cargo build ``` -Once it has built, and you have confirmed there are no warnings or errors, you should commit your changes. +Once it has built, check for common code mistakes by running: +```bash +cargo clippy +``` + +Once you have confirmed there are no warnings or errors, you should commit your changes. ```bash git commit -a -m "My fancy fix" diff --git a/README.md b/README.md index bcf73cac..f557cbc4 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,17 @@ [![Gitter chat](https://badges.gitter.im/librespot-org/librespot.png)](https://gitter.im/librespot-org/spotify-connect-resources) [![Crates.io](https://img.shields.io/crates/v/librespot.svg)](https://crates.io/crates/librespot) -Current maintainer is [@awiouy](https://github.com/awiouy) folks. +Current maintainers are [listed on GitHub](https://github.com/orgs/librespot-org/people). # librespot *librespot* is an open source client library for Spotify. It enables applications to use Spotify's service to control and play music via various backends, and to act as a Spotify Connect receiver. It is an alternative to the official and [now deprecated](https://pyspotify.mopidy.com/en/latest/#libspotify-s-deprecation) closed-source `libspotify`. Additionally, it will provide extra features which are not available in the official library. -_Note: librespot only works with Spotify Premium. This will remain the case for the foreseeable future, as we are unlikely to work on implementing the features such as limited skips and adverts that would be required to make librespot compliant with free accounts._ +_Note: librespot only works with Spotify Premium. This will remain the case. We will not any support features to make librespot compatible with free accounts, such as limited skips and adverts._ ## Quick start We're available on [crates.io](https://crates.io/crates/librespot) as the _librespot_ package. Simply run `cargo install librespot` to install librespot on your system. Check the wiki for more info and possible [usage options](https://github.com/librespot-org/librespot/wiki/Options). -After installation, you can run librespot from the CLI using a command such as `librespot -n "Librespot Speaker" -b 160` to create a speaker called _Librespot Speaker_ serving 160kbps audio. +After installation, you can run librespot from the CLI using a command such as `librespot -n "Librespot Speaker" -b 160` to create a speaker called _Librespot Speaker_ serving 160 kbps audio. ## This fork As the origin by [plietar](https://github.com/plietar/) is no longer actively maintained, this organisation and repository have been set up so that the project may be maintained and upgraded in the future. @@ -53,12 +53,14 @@ librespot currently offers the following selection of [audio backends](https://g ``` Rodio (default) ALSA +GStreamer PortAudio PulseAudio JACK JACK over Rodio SDL Pipe +Subprocess ``` Please check the corresponding [compiling entry](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) for backend specific dependencies. @@ -84,9 +86,9 @@ The above is a minimal example. Here is a more fully fledged one: ```shell target/release/librespot -n "Librespot" -b 320 -c ./cache --enable-volume-normalisation --initial-volume 75 --device-type avr ``` -The above command will create a receiver named ```Librespot```, with bitrate set to 320kbps, initial volume at 75%, with volume normalisation enabled, and the device displayed in the app as an Audio/Video Receiver. A folder named ```cache``` will be created/used in the current directory, and be used to cache audio data and credentials. +The above command will create a receiver named ```Librespot```, with bitrate set to 320 kbps, initial volume at 75%, with volume normalisation enabled, and the device displayed in the app as an Audio/Video Receiver. A folder named ```cache``` will be created/used in the current directory, and be used to cache audio data and credentials. -A full list of runtime options are available [here](https://github.com/librespot-org/librespot/wiki/Options) +A full list of runtime options is available [here](https://github.com/librespot-org/librespot/wiki/Options). _Please Note: When using the cache feature, an authentication blob is stored for your account in the cache directory. For security purposes, we recommend that you set directory permissions on the cache directory to `700`._ From 43a8b91a3dd8715d777520810642503385e19a8f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Jul 2021 22:17:29 +0200 Subject: [PATCH 09/95] Revert name to softvol --- playback/src/mixer/softmixer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 27448237..cefc2de5 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -43,7 +43,7 @@ impl Mixer for SoftMixer { } impl SoftMixer { - pub const NAME: &'static str = "softmixer"; + pub const NAME: &'static str = "softvol"; } struct SoftVolumeApplier { From bd350c5aa0ebbea0769e11be79258d7087c9220b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Jul 2021 22:30:49 +0200 Subject: [PATCH 10/95] Remove non-working Facebook authentication --- docs/authentication.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/docs/authentication.md b/docs/authentication.md index 86470161..2eeb5645 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -57,20 +57,4 @@ login_data = AES192-DECRYPT(key, data) ``` ## Facebook based Authentication -The client starts an HTTPS server, and makes the user visit -`https://login.spotify.com/login-facebook-sso/?csrf=CSRF&port=PORT` -in their browser, where CSRF is a random token, and PORT is the HTTPS server's port. - -This will redirect to Facebook, where the user must login and authorize Spotify, and -finally make a GET request to -`https://login.spotilocal.com:PORT/login/facebook_login_sso.json?csrf=CSRF&access_token=TOKEN`, -where PORT and CSRF are the same as sent earlier, and TOKEN is the facebook authentication token. - -Since `login.spotilocal.com` resolves the 127.0.0.1, the request is received by the client. - -The client must then contact Facebook's API at -`https://graph.facebook.com/me?fields=id&access_token=TOKEN` -in order to retrieve the user's Facebook ID. - -The Facebook ID is the `username`, the TOKEN the `auth_data`, and `auth_type` is set to `AUTHENTICATION_FACEBOOK_TOKEN`. - +Facebook authentication is currently broken due to Spotify changing the authentication flow. The details of how the new flow works are detailed in https://github.com/librespot-org/librespot/issues/244 and will be implemented at some point in the future. From efd4a02896389735f541b27353558eb2fe0a1134 Mon Sep 17 00:00:00 2001 From: sigaloid <69441971+sigaloid@users.noreply.github.com> Date: Fri, 20 Aug 2021 16:13:39 -0400 Subject: [PATCH 11/95] Cargo update --- Cargo.lock | 342 +++++++++++++++++++++++++++++------------------------ 1 file changed, 187 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 64695723..6d631c83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,15 +78,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.41" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" +checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" [[package]] name = "async-trait" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722" +checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" dependencies = [ "proc-macro2", "quote", @@ -170,9 +170,9 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" dependencies = [ "jobserver", ] @@ -260,9 +260,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.6.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" [[package]] name = "coreaudio-rs" @@ -285,21 +285,21 @@ dependencies = [ [[package]] name = "cpal" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8351ddf2aaa3c583fa388029f8b3d26f3c7035a20911fdd5f2e2ed7ab57dad25" +checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" dependencies = [ "alsa", "core-foundation-sys", "coreaudio-rs", - "jack 0.6.6", + "jack", "jni", "js-sys", "lazy_static", "libc", "mach", - "ndk", - "ndk-glue", + "ndk 0.3.0", + "ndk-glue 0.3.0", "nix", "oboe", "parking_lot", @@ -320,9 +320,9 @@ dependencies = [ [[package]] name = "crypto-mac" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ "generic-array", "subtle", @@ -439,9 +439,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" +checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" dependencies = [ "futures-channel", "futures-core", @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" +checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" dependencies = [ "futures-core", "futures-sink", @@ -464,15 +464,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" +checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" [[package]] name = "futures-executor" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" +checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" dependencies = [ "futures-core", "futures-task", @@ -481,15 +481,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" +checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" [[package]] name = "futures-macro" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" +checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" dependencies = [ "autocfg", "proc-macro-hack", @@ -500,21 +500,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" +checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" [[package]] name = "futures-task" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" +checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" [[package]] name = "futures-util" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" +checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" dependencies = [ "autocfg", "futures-channel", @@ -589,7 +589,7 @@ dependencies = [ "anyhow", "heck", "itertools", - "proc-macro-crate", + "proc-macro-crate 0.1.5", "proc-macro-error", "proc-macro2", "quote", @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" +checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" dependencies = [ "bytes", "http", @@ -821,9 +821,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" [[package]] name = "httpdate" @@ -839,9 +839,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.9" +version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83" +checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" dependencies = [ "bytes", "futures-channel", @@ -925,9 +925,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if 1.0.0", ] @@ -947,18 +947,6 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" -[[package]] -name = "jack" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb4974bd7e6b2fb7784f27fa13d819d11292b3b004dce0185ec08163cf686a" -dependencies = [ - "bitflags", - "jack-sys", - "lazy_static", - "libc", -] - [[package]] name = "jack" version = "0.7.1" @@ -984,9 +972,9 @@ dependencies = [ [[package]] name = "jni" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" dependencies = [ "cesu8", "combine", @@ -1004,18 +992,18 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.22" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d" dependencies = [ "wasm-bindgen", ] @@ -1045,9 +1033,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" [[package]] name = "libloading" @@ -1095,9 +1083,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.23.1" +version = "2.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db951f37898e19a6785208e3a290261e0f1a8e086716be596aaad684882ca8e3" +checksum = "04b4154b9bc606019cb15125f96e08e1e9c4f53d55315f1ef69ae229e30d1765" dependencies = [ "bitflags", "libc", @@ -1109,9 +1097,9 @@ dependencies = [ [[package]] name = "libpulse-simple-binding" -version = "2.23.0" +version = "2.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a574975292db859087c3957b9182f7d53278553f06bddaa2099c90e4ac3a0ee0" +checksum = "1165af13c42b9c325582b1a75eaa4a0f176c9094bb3a13877826e9be24881231" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -1120,9 +1108,9 @@ dependencies = [ [[package]] name = "libpulse-simple-sys" -version = "1.16.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "468cf582b7b022c0d1b266fefc7fc8fa7b1ddcb61214224f2f105c95a9c2d5c1" +checksum = "83346d68605e656afdefa9a8a2f1968fa05ab9369b55f2e26f7bf2a11b7e8444" dependencies = [ "libpulse-sys", "pkg-config", @@ -1130,9 +1118,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.18.0" +version = "1.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf17e9832643c4f320c42b7d78b2c0510f45aa5e823af094413b94e45076ba82" +checksum = "9ebed2cc92c38cac12307892ce6fb17e2e950bfda1ed17b3e1d47fd5184c8f2b" dependencies = [ "libc", "num-derive", @@ -1288,7 +1276,7 @@ dependencies = [ "glib", "gstreamer", "gstreamer-app", - "jack 0.7.1", + "jack", "lewton", "libpulse-binding", "libpulse-simple-binding", @@ -1352,15 +1340,24 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matches" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] [[package]] name = "mime" @@ -1417,6 +1414,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ndk" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + [[package]] name = "ndk-glue" version = "0.3.0" @@ -1426,7 +1436,21 @@ dependencies = [ "lazy_static", "libc", "log", - "ndk", + "ndk 0.3.0", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-glue" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.4.0", "ndk-macro", "ndk-sys", ] @@ -1438,7 +1462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" dependencies = [ "darling", - "proc-macro-crate", + "proc-macro-crate 0.1.5", "proc-macro2", "quote", "syn", @@ -1452,14 +1476,15 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" [[package]] name = "nix" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +checksum = "df8e5e343312e7fbeb2a52139114e9e702991ef9c2aea6817ff2440b35647d56" dependencies = [ "bitflags", "cc", "cfg-if 1.0.0", "libc", + "memoffset", ] [[package]] @@ -1547,9 +1572,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066" +checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" dependencies = [ "derivative", "num_enum_derive", @@ -1557,11 +1582,11 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.5.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e" +checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.0.0", "proc-macro2", "quote", "syn", @@ -1569,13 +1594,13 @@ dependencies = [ [[package]] name = "oboe" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa187b38ae20374617b7ad418034ed3dc90ac980181d211518bd03537ae8f8d" +checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1" dependencies = [ "jni", - "ndk", - "ndk-glue", + "ndk 0.4.0", + "ndk-glue 0.4.0", "num-derive", "num-traits", "oboe-sys", @@ -1583,9 +1608,9 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88e64835aa3f579c08d182526dc34e3907343d5b97e87b71a40ba5bca7aca9e" +checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc" dependencies = [ "cc", ] @@ -1734,6 +1759,16 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-crate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" +dependencies = [ + "thiserror", + "toml", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1772,33 +1807,33 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" dependencies = [ "unicode-xid", ] [[package]] name = "protobuf" -version = "2.24.1" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db50e77ae196458ccd3dc58a31ea1a90b0698ab1b7928d89f644c25d72070267" +checksum = "020f86b07722c5c4291f7c723eac4676b3892d47d9a7708dc2779696407f039b" [[package]] name = "protobuf-codegen" -version = "2.24.1" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09321cef9bee9ddd36884f97b7f7cc92a586cdc74205c4b3aeba65b5fc9c6f90" +checksum = "7b8ac7c5128619b0df145d9bace18e8ed057f18aebda1aa837a5525d4422f68c" dependencies = [ "protobuf", ] [[package]] name = "protobuf-codegen-pure" -version = "2.24.1" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1afb68a6d768571da3db86ce55f0f62966e0fc25eaf96acd070ea548a91b0d23" +checksum = "f6d0daa1b61d6e7a128cdca8c8604b3c5ee22c424c15c8d3a92fafffeda18aaf" dependencies = [ "protobuf", "protobuf-codegen", @@ -1865,9 +1900,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -1978,24 +2013,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f3aac57ee7f3272d8395c6e4f502f434f0e289fcd62876f70daa008c20dcabe" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.126" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" dependencies = [ "proc-macro2", "quote", @@ -2004,9 +2039,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" dependencies = [ "itoa", "ryu", @@ -2015,9 +2050,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16" +checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" dependencies = [ "block-buffer", "cfg-if 1.0.0", @@ -2058,9 +2093,9 @@ dependencies = [ [[package]] name = "simple_logger" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd57f17c093ead1d4a1499dc9acaafdd71240908d64775465543b8d9a9f1d198" +checksum = "b7de33c687404ec3045d4a0d437580455257c0436f858d702f244e7d652f9f07" dependencies = [ "atty", "chrono", @@ -2071,9 +2106,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" [[package]] name = "smallvec" @@ -2083,9 +2118,9 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" [[package]] name = "socket2" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" dependencies = [ "libc", "winapi", @@ -2123,15 +2158,15 @@ dependencies = [ [[package]] name = "subtle" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" dependencies = [ "proc-macro2", "quote", @@ -2140,9 +2175,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" dependencies = [ "proc-macro2", "quote", @@ -2190,18 +2225,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" dependencies = [ "proc-macro2", "quote", @@ -2220,9 +2255,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" dependencies = [ "tinyvec_macros", ] @@ -2235,9 +2270,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.7.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2" +checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" dependencies = [ "autocfg", "bytes", @@ -2254,9 +2289,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" dependencies = [ "proc-macro2", "quote", @@ -2265,9 +2300,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" +checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" dependencies = [ "futures-core", "pin-project-lite", @@ -2316,9 +2351,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8" dependencies = [ "lazy_static", ] @@ -2337,12 +2372,9 @@ checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" [[package]] name = "unicode-bidi" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" -dependencies = [ - "matches", -] +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" [[package]] name = "unicode-normalization" @@ -2355,9 +2387,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-width" @@ -2444,9 +2476,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -2454,9 +2486,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041" dependencies = [ "bumpalo", "lazy_static", @@ -2469,9 +2501,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2479,9 +2511,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad" dependencies = [ "proc-macro2", "quote", @@ -2492,15 +2524,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" +checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" [[package]] name = "web-sys" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c" dependencies = [ "js-sys", "wasm-bindgen", From c67e268dc8b9552e215271cce7bb638a3133b3a2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 26 Aug 2021 22:35:45 +0200 Subject: [PATCH 12/95] Improve Alsa mixer command-line options --- CHANGELOG.md | 5 +- playback/src/mixer/alsamixer.rs | 16 +++--- playback/src/mixer/mod.rs | 4 +- src/main.rs | 98 ++++++++++++++++++++++++--------- 4 files changed, 87 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ecd12f2..acf4f735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,11 +29,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated - [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate +- [playback] `alsamixer`: renamed `mixer-card` to `alsa-mixer-device` +- [playback] `alsamixer`: renamed `mixer-name` to `alsa-mixer-control` +- [playback] `alsamixer`: renamed `mixer-index` to `alsa-mixer-index` ### Removed - [connect] Removed no-op mixer started/stopped logic (breaking) - [playback] Removed `with-vorbis` and `with-tremor` features -- [playback] `alsamixer`: removed `--mixer-linear-volume` option; use `--volume-ctrl linear` instead +- [playback] `alsamixer`: removed `--mixer-linear-volume` option, now that `--volume-ctrl {linear|log}` work as expected on Alsa ### Fixed - [connect] Fix step size on volume up/down events diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 8bee9e0d..81d0436f 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -31,14 +31,14 @@ const ZERO_DB: MilliBel = MilliBel(0); impl Mixer for AlsaMixer { fn open(config: MixerConfig) -> Self { info!( - "Mixing with alsa and volume control: {:?} for card: {} with mixer control: {},{}", - config.volume_ctrl, config.card, config.control, config.index, + "Mixing with Alsa and volume control: {:?} for device: {} with mixer control: {},{}", + config.volume_ctrl, config.device, config.control, config.index, ); let mut config = config; // clone let mixer = - alsa::mixer::Mixer::new(&config.card, false).expect("Could not open Alsa mixer"); + alsa::mixer::Mixer::new(&config.device, false).expect("Could not open Alsa mixer"); let simple_element = mixer .find_selem(&SelemId::new(&config.control, config.index)) .expect("Could not find Alsa mixer control"); @@ -56,8 +56,8 @@ impl Mixer for AlsaMixer { // Query dB volume range -- note that Alsa exposes a different // API for hardware and software mixers let (min_millibel, max_millibel) = if is_softvol { - let control = - Ctl::new(&config.card, false).expect("Could not open Alsa softvol with that card"); + let control = Ctl::new(&config.device, false) + .expect("Could not open Alsa softvol with that device"); let mut element_id = ElemId::new(ElemIface::Mixer); element_id.set_name( &CString::new(config.control.as_str()) @@ -144,7 +144,7 @@ impl Mixer for AlsaMixer { fn volume(&self) -> u16 { let mixer = - alsa::mixer::Mixer::new(&self.config.card, false).expect("Could not open Alsa mixer"); + alsa::mixer::Mixer::new(&self.config.device, false).expect("Could not open Alsa mixer"); let simple_element = mixer .find_selem(&SelemId::new(&self.config.control, self.config.index)) .expect("Could not find Alsa mixer control"); @@ -184,7 +184,7 @@ impl Mixer for AlsaMixer { fn set_volume(&self, volume: u16) { let mixer = - alsa::mixer::Mixer::new(&self.config.card, false).expect("Could not open Alsa mixer"); + alsa::mixer::Mixer::new(&self.config.device, false).expect("Could not open Alsa mixer"); let simple_element = mixer .find_selem(&SelemId::new(&self.config.control, self.config.index)) .expect("Could not find Alsa mixer control"); @@ -249,7 +249,7 @@ impl AlsaMixer { } let mixer = - alsa::mixer::Mixer::new(&self.config.card, false).expect("Could not open Alsa mixer"); + alsa::mixer::Mixer::new(&self.config.device, false).expect("Could not open Alsa mixer"); let simple_element = mixer .find_selem(&SelemId::new(&self.config.control, self.config.index)) .expect("Could not find Alsa mixer control"); diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index ed39582e..5397598f 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -30,7 +30,7 @@ use self::alsamixer::AlsaMixer; #[derive(Debug, Clone)] pub struct MixerConfig { - pub card: String, + pub device: String, pub control: String, pub index: u32, pub volume_ctrl: VolumeCtrl, @@ -39,7 +39,7 @@ pub struct MixerConfig { impl Default for MixerConfig { fn default() -> MixerConfig { MixerConfig { - card: String::from("default"), + device: String::from("default"), control: String::from("PCM"), index: 0, volume_ctrl: VolumeCtrl::default(), diff --git a/src/main.rs b/src/main.rs index aa04d0d4..d240e224 100644 --- a/src/main.rs +++ b/src/main.rs @@ -206,9 +206,9 @@ fn get_setup(args: &[String]) -> Setup { const HELP: &str = "h"; const INITIAL_VOLUME: &str = "initial-volume"; const MIXER_TYPE: &str = "mixer"; - const MIXER_CARD: &str = "mixer-card"; - const MIXER_INDEX: &str = "mixer-index"; - const MIXER_NAME: &str = "mixer-name"; + const ALSA_MIXER_DEVICE: &str = "alsa-mixer-device"; + const ALSA_MIXER_INDEX: &str = "alsa-mixer-index"; + const ALSA_MIXER_CONTROL: &str = "alsa-mixer-control"; const NAME: &str = "name"; const NORMALISATION_ATTACK: &str = "normalisation-attack"; const NORMALISATION_GAIN_TYPE: &str = "normalisation-gain-type"; @@ -296,24 +296,42 @@ fn get_setup(args: &[String]) -> Setup { "Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", "DITHER", ) - .optopt("", MIXER_TYPE, "Mixer to use {alsa|softvol}.", "MIXER") + .optopt("m", MIXER_TYPE, "Mixer to use {alsa|softvol}.", "MIXER") .optopt( - "m", - MIXER_NAME, + "", + "mixer-name", // deprecated + "", + "", + ) + .optopt( + "", + ALSA_MIXER_CONTROL, "Alsa mixer control, e.g. 'PCM' or 'Master'. Defaults to 'PCM'.", "NAME", ) .optopt( "", - MIXER_CARD, - "Alsa mixer card, e.g 'hw:0' or similar from `aplay -l`. Defaults to DEVICE if specified, 'default' otherwise.", - "MIXER_CARD", + "mixer-card", // deprecated + "", + "", ) .optopt( "", - MIXER_INDEX, + ALSA_MIXER_DEVICE, + "Alsa mixer device, e.g 'hw:0' or similar from `aplay -l`. Defaults to `--device` if specified, 'default' otherwise.", + "DEVICE", + ) + .optopt( + "", + "mixer-index", // deprecated + "", + "", + ) + .optopt( + "", + ALSA_MIXER_INDEX, "Alsa index of the cards mixer. Defaults to 0.", - "INDEX", + "NUMBER", ) .optopt( "", @@ -459,20 +477,50 @@ fn get_setup(args: &[String]) -> Setup { let mixer = mixer::find(mixer_type.as_deref()).expect("Invalid mixer"); let mixer_config = { - let card = matches.opt_str(MIXER_CARD).unwrap_or_else(|| { - if let Some(ref device_name) = device { - device_name.to_string() - } else { - MixerConfig::default().card + let mixer_device = match matches.opt_str("mixer-card") { + Some(card) => { + warn!("--mixer-card is deprecated and will be removed in a future release."); + warn!("Please use --alsa-mixer-device instead."); + card } - }); - let index = matches - .opt_str(MIXER_INDEX) - .map(|index| index.parse::().unwrap()) - .unwrap_or(0); - let control = matches - .opt_str(MIXER_NAME) - .unwrap_or_else(|| MixerConfig::default().control); + None => matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { + if let Some(ref device_name) = device { + device_name.to_string() + } else { + MixerConfig::default().device + } + }), + }; + + let index = match matches.opt_str("mixer-index") { + Some(index) => { + warn!("--mixer-index is deprecated and will be removed in a future release."); + warn!("Please use --alsa-mixer-index instead."); + index + .parse::() + .expect("Mixer index is not a valid number") + } + None => matches + .opt_str(ALSA_MIXER_INDEX) + .map(|index| { + index + .parse::() + .expect("Alsa mixer index is not a valid number") + }) + .unwrap_or(0), + }; + + let control = match matches.opt_str("mixer-name") { + Some(name) => { + warn!("--mixer-name is deprecated and will be removed in a future release."); + warn!("Please use --alsa-mixer-control instead."); + name + } + None => matches + .opt_str(ALSA_MIXER_CONTROL) + .unwrap_or_else(|| MixerConfig::default().control), + }; + let mut volume_range = matches .opt_str(VOLUME_RANGE) .map(|range| range.parse::().unwrap()) @@ -503,7 +551,7 @@ fn get_setup(args: &[String]) -> Setup { }); MixerConfig { - card, + device: mixer_device, control, index, volume_ctrl, From 7da4d0e4730ecdc8fcf82c97f1bc466ddf28e8b3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 1 Sep 2021 20:54:47 +0200 Subject: [PATCH 13/95] Attenuate after normalisation --- playback/src/player.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index a6e71aad..21afdbbe 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1186,10 +1186,6 @@ impl PlayerInternal { Some(mut packet) => { if !packet.is_empty() { if let AudioPacket::Samples(ref mut data) = packet { - if let Some(ref editor) = self.audio_filter { - editor.modify_stream(data) - } - if self.config.normalisation && !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON && self.config.normalisation_method == NormalisationMethod::Basic) @@ -1302,6 +1298,10 @@ impl PlayerInternal { } } } + + if let Some(ref editor) = self.audio_filter { + editor.modify_stream(data) + } } if let Err(err) = self.sink.write(&packet, &mut self.converter) { From d8e35bf0c4f9ee3909da276665ac1d9df8386e00 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 1 Sep 2021 20:55:28 +0200 Subject: [PATCH 14/95] Remove clamping of float samples --- playback/src/player.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 21afdbbe..361c24a7 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1286,16 +1286,7 @@ impl PlayerInternal { } } } - *sample *= actual_normalisation_factor; - - // Extremely sharp attacks, however unlikely, *may* still clip and provide - // undefined results, so strictly enforce output within [-1.0, 1.0]. - if *sample < -1.0 { - *sample = -1.0; - } else if *sample > 1.0 { - *sample = 1.0; - } } } From b016b697722a09663215122bb7fd16061822e223 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 1 Sep 2021 21:25:32 +0200 Subject: [PATCH 15/95] Fix clippy warnings --- audio/src/decrypt.rs | 4 ++-- connect/src/spirc.rs | 6 +++--- core/src/connection/handshake.rs | 2 +- metadata/src/lib.rs | 2 +- playback/src/audio_backend/alsa.rs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/audio/src/decrypt.rs b/audio/src/decrypt.rs index 616ef4f6..17f4edba 100644 --- a/audio/src/decrypt.rs +++ b/audio/src/decrypt.rs @@ -18,8 +18,8 @@ pub struct AudioDecrypt { impl AudioDecrypt { pub fn new(key: AudioKey, reader: T) -> AudioDecrypt { let cipher = Aes128Ctr::new( - &GenericArray::from_slice(&key.0), - &GenericArray::from_slice(&AUDIO_AESIV), + GenericArray::from_slice(&key.0), + GenericArray::from_slice(&AUDIO_AESIV), ); AudioDecrypt { cipher, reader } } diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 57dc4cdd..9c541871 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1033,7 +1033,7 @@ impl SpircTask { .payload .first() .expect("Empty payload on context uri"); - let response: serde_json::Value = serde_json::from_slice(&data).unwrap(); + let response: serde_json::Value = serde_json::from_slice(data).unwrap(); Ok(response) } @@ -1051,7 +1051,7 @@ impl SpircTask { if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { track_vec.drain(0..head); } - track_vec.extend_from_slice(&new_tracks); + track_vec.extend_from_slice(new_tracks); self.state .set_track(protobuf::RepeatedField::from_vec(track_vec)); @@ -1218,7 +1218,7 @@ impl SpircTask { trace!("Sending status to server: [{}]", status_string); let mut cs = CommandSender::new(self, MessageType::kMessageTypeNotify); if let Some(s) = recipient { - cs = cs.recipient(&s); + cs = cs.recipient(s); } cs.send(); } diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 82ec7672..eddcd327 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -124,7 +124,7 @@ fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec, Vec, Vec< let mut data = Vec::with_capacity(0x64); for i in 1..6 { let mut mac = - HmacSha1::new_from_slice(&shared_secret).expect("HMAC can take key of any size"); + HmacSha1::new_from_slice(shared_secret).expect("HMAC can take key of any size"); mac.update(packets); mac.update(&[i]); data.extend_from_slice(&mac.finalize().into_bytes()); diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index d328a7d9..cf663ce6 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -126,7 +126,7 @@ pub trait Metadata: Send + Sized + 'static { let data = response.payload.first().expect("Empty payload"); let msg = Self::Message::parse_from_bytes(data).unwrap(); - Ok(Self::parse(&msg, &session)) + Ok(Self::parse(&msg, session)) } } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 7b5987a3..8b8962fb 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -59,7 +59,7 @@ fn list_outputs() -> io::Result<()> { println!("Listing available Alsa outputs:"); for t in &["pcm", "ctl", "hwdep"] { println!("{} devices:", t); - let i = match HintIter::new_str(None, &t) { + let i = match HintIter::new_str(None, t) { Ok(i) => i, Err(e) => { return Err(io::Error::new(io::ErrorKind::Other, e)); From fe644bc0d7bf8fb92c256965f24a9aa3253840a1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 2 Sep 2021 22:04:30 +0200 Subject: [PATCH 16/95] Update default normalisation threshold --- CHANGELOG.md | 1 + playback/src/config.rs | 2 +- src/main.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acf4f735..834b0bbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise - [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking) - [playback] `player`: make `convert` and `decoder` public so you can implement your own `Sink` +- [playback] Updated default normalisation threshold to -2 dBFS ### Deprecated - [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate diff --git a/playback/src/config.rs b/playback/src/config.rs index 7604f59f..14c9cf38 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -151,7 +151,7 @@ impl Default for PlayerConfig { normalisation_type: NormalisationType::default(), normalisation_method: NormalisationMethod::default(), normalisation_pregain: 0.0, - normalisation_threshold: db_to_ratio(-1.0), + normalisation_threshold: db_to_ratio(-2.0), normalisation_attack: Duration::from_millis(5), normalisation_release: Duration::from_millis(100), normalisation_knee: 1.0, diff --git a/src/main.rs b/src/main.rs index d240e224..c896a303 100644 --- a/src/main.rs +++ b/src/main.rs @@ -371,7 +371,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", NORMALISATION_THRESHOLD, - "Threshold (dBFS) to prevent clipping. Defaults to -1.0.", + "Threshold (dBFS) to prevent clipping. Defaults to -2.0.", "THRESHOLD", ) .optopt( From 9cb98e9e2180f6de928f9c47872de1379adedf8d Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 2 Sep 2021 22:41:12 +0200 Subject: [PATCH 17/95] Fix typos and define what's "breaking" --- CONTRIBUTING.md | 2 +- README.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 907a7c04..1ba24393 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ In order to prepare for a PR, you will need to do a couple of things first: Make any changes that you are going to make to the code, but do not commit yet. -Unless your changes are negligible, please add an entry in the "Unreleased" section of `CHANGELOG.md`. Refer to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) for instructions on how this entry should look like. +Unless your changes are negligible, please add an entry in the "Unreleased" section of `CHANGELOG.md`. Refer to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) for instructions on how this entry should look like. If your changes break the API such that downstream packages that depend on librespot need to update their source to still compile, you should mark your changes as `(breaking)`. Make sure that the code is correctly formatted by running: ```bash diff --git a/README.md b/README.md index f557cbc4..20afc01b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Current maintainers are [listed on GitHub](https://github.com/orgs/librespot-org # librespot *librespot* is an open source client library for Spotify. It enables applications to use Spotify's service to control and play music via various backends, and to act as a Spotify Connect receiver. It is an alternative to the official and [now deprecated](https://pyspotify.mopidy.com/en/latest/#libspotify-s-deprecation) closed-source `libspotify`. Additionally, it will provide extra features which are not available in the official library. -_Note: librespot only works with Spotify Premium. This will remain the case. We will not any support features to make librespot compatible with free accounts, such as limited skips and adverts._ +_Note: librespot only works with Spotify Premium. This will remain the case. We will not support any features to make librespot compatible with free accounts, such as limited skips and adverts._ ## Quick start We're available on [crates.io](https://crates.io/crates/librespot) as the _librespot_ package. Simply run `cargo install librespot` to install librespot on your system. Check the wiki for more info and possible [usage options](https://github.com/librespot-org/librespot/wiki/Options). @@ -20,7 +20,7 @@ As the origin by [plietar](https://github.com/plietar/) is no longer actively ma # Documentation Documentation is currently a work in progress, contributions are welcome! -There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder, +There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder. [COMPILING.md](https://github.com/librespot-org/librespot/blob/master/COMPILING.md) contains detailed instructions on setting up a development environment, and compiling librespot. More general usage and compilation information is available on the [wiki](https://github.com/librespot-org/librespot/wiki). [CONTRIBUTING.md](https://github.com/librespot-org/librespot/blob/master/CONTRIBUTING.md) also contains our contributing guidelines. @@ -30,26 +30,26 @@ If you wish to learn more about how librespot works overall, the best way is to # Issues & Discussions **We have recently started using Github discussions for general questions and feature requests, as they are a more natural medium for such cases, and allow for upvoting to prioritize feature development. Check them out [here](https://github.com/librespot-org/librespot/discussions). Bugs and issues with the underlying library should still be reported as issues.** -If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, eg. the Spotify URI of the song that caused the crash. +If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, e.g. the Spotify URI of the song that caused the crash. # Building -A quick walk through of the build process is outlined here, while a detailed compilation guide can be found [here](https://github.com/librespot-org/librespot/blob/master/COMPILING.md). +A quick walkthrough of the build process is outlined below, while a detailed compilation guide can be found [here](https://github.com/librespot-org/librespot/blob/master/COMPILING.md). ## Additional Dependencies We recently switched to using [Rodio](https://github.com/tomaka/rodio) for audio playback by default, hence for macOS and Windows, you should just be able to clone and build librespot (with the command below). For Linux, you will need to run the additional commands below, depending on your distro. -On Debian/Ubuntu, the following command will install these dependencies : +On Debian/Ubuntu, the following command will install these dependencies: ```shell sudo apt-get install build-essential libasound2-dev ``` -On Fedora systems, the following command will install these dependencies : +On Fedora systems, the following command will install these dependencies: ```shell sudo dnf install alsa-lib-devel make gcc ``` -librespot currently offers the following selection of [audio backends](https://github.com/librespot-org/librespot/wiki/Audio-Backends). +librespot currently offers the following selection of [audio backends](https://github.com/librespot-org/librespot/wiki/Audio-Backends): ``` Rodio (default) ALSA @@ -62,7 +62,7 @@ SDL Pipe Subprocess ``` -Please check the corresponding [compiling entry](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) for backend specific dependencies. +Please check the corresponding [Compiling](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) entry on the wiki for backend specific dependencies. Once you've installed the dependencies and cloned this repository you can build *librespot* with the default backend using Cargo. ```shell @@ -93,7 +93,7 @@ A full list of runtime options is available [here](https://github.com/librespot- _Please Note: When using the cache feature, an authentication blob is stored for your account in the cache directory. For security purposes, we recommend that you set directory permissions on the cache directory to `700`._ ## Contact -Come and hang out on gitter if you need help or want to offer some. +Come and hang out on gitter if you need help or want to offer some: https://gitter.im/librespot-org/spotify-connect-resources ## Disclaimer From 7401d6a96eab95950a360d7ea890b828684bb964 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 20 Sep 2021 14:20:44 -0300 Subject: [PATCH 18/95] Don't panic on local files (#846) Skip tracks whose Spotify ID can't be found --- CHANGELOG.md | 1 + metadata/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 834b0bbf..83bf64fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `alsa`, `gstreamer`, `pulseaudio`: always output in native endianness - [playback] `alsa`: revert buffer size to ~500 ms - [playback] `alsa`, `pipe`, `pulseaudio`: better error handling +- [metadata] Skip tracks whose Spotify ID's can't be found (e.g. local files, which aren't supported) ## [0.2.0] - 2021-05-04 diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index cf663ce6..2ed9273e 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -291,10 +291,10 @@ impl Metadata for Playlist { .get_contents() .get_items() .iter() - .map(|item| { + .filter_map(|item| { let uri_split = item.get_uri().split(':'); let uri_parts: Vec<&str> = uri_split.collect(); - SpotifyId::from_base62(uri_parts[2]).unwrap() + SpotifyId::from_base62(uri_parts[2]).ok() }) .collect::>(); From 949ca4fded0ca399f1ec68e090aac4fd83f4ac59 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 20 Sep 2021 19:22:02 +0200 Subject: [PATCH 19/95] Add and default to "auto" normalisation type (#844) --- CHANGELOG.md | 4 +- audio/src/fetch/receive.rs | 3 +- connect/src/spirc.rs | 11 +++++- playback/src/config.rs | 6 ++- playback/src/player.rs | 81 +++++++++++++++++++++++++++++--------- src/main.rs | 2 +- 6 files changed, 82 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83bf64fd..6235b017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves - [playback] `alsamixer`: support for querying dB range from Alsa softvol - [playback] Add `--format F64` (supported by Alsa and GStreamer only) +- [playback] Add `--normalisation-type auto` that switches between album and track automatically ### Changed - [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) @@ -26,7 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise - [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking) - [playback] `player`: make `convert` and `decoder` public so you can implement your own `Sink` -- [playback] Updated default normalisation threshold to -2 dBFS +- [playback] `player`: update default normalisation threshold to -2 dBFS +- [playback] `player`: default normalisation type is now `auto` ### Deprecated - [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 64becc23..f7574f4f 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -266,7 +266,8 @@ impl AudioFileFetch { fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow { match data { ReceivedData::ResponseTime(response_time) => { - trace!("Ping time estimated as: {}ms", response_time.as_millis()); + // chatty + // trace!("Ping time estimated as: {}ms", response_time.as_millis()); // prune old response times. Keep at most two so we can push a third. while self.network_response_times.len() >= 3 { diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 9c541871..9aa86134 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -902,7 +902,8 @@ impl SpircTask { self.context_fut = self.resolve_station(&context_uri); self.update_tracks_from_context(); } - if self.config.autoplay && new_index == tracks_len - 1 { + let last_track = new_index == tracks_len - 1; + if self.config.autoplay && last_track { // Extend the playlist // Note: This doesn't seem to reflect in the UI // the additional tracks in the frame don't show up as with station view @@ -917,6 +918,11 @@ impl SpircTask { if tracks_len > 0 { self.state.set_playing_track_index(new_index); self.load_track(continue_playing, 0); + if self.config.autoplay && last_track { + // If we're now playing the last track of an album, then + // switch to track normalisation mode for the autoplay to come. + self.player.set_auto_normalise_as_album(false); + } } else { info!("Not playing next track because there are no more tracks left in queue."); self.state.set_playing_track_index(0); @@ -1084,6 +1090,9 @@ impl SpircTask { self.autoplay_fut = self.resolve_autoplay_uri(&context_uri); } + self.player + .set_auto_normalise_as_album(context_uri.starts_with("spotify:album:")); + self.state.set_playing_track_index(index); self.state.set_track(tracks.iter().cloned().collect()); self.state.set_context_uri(context_uri); diff --git a/playback/src/config.rs b/playback/src/config.rs index 14c9cf38..c442faee 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -76,10 +76,11 @@ impl AudioFormat { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum NormalisationType { Album, Track, + Auto, } impl FromStr for NormalisationType { @@ -88,6 +89,7 @@ impl FromStr for NormalisationType { match s.to_lowercase().as_ref() { "album" => Ok(Self::Album), "track" => Ok(Self::Track), + "auto" => Ok(Self::Auto), _ => Err(()), } } @@ -95,7 +97,7 @@ impl FromStr for NormalisationType { impl Default for NormalisationType { fn default() -> Self { - Self::Album + Self::Auto } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 361c24a7..d858e333 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -67,6 +67,8 @@ struct PlayerInternal { limiter_peak_sample: f64, limiter_factor: f64, limiter_strength: f64, + + auto_normalise_as_album: bool, } enum PlayerCommand { @@ -86,6 +88,7 @@ enum PlayerCommand { AddEventSender(mpsc::UnboundedSender), SetSinkEventCallback(Option), EmitVolumeSetEvent(u16), + SetAutoNormaliseAsAlbum(bool), } #[derive(Debug, Clone)] @@ -238,9 +241,10 @@ impl NormalisationData { return 1.0; } - let [gain_db, gain_peak] = match config.normalisation_type { - NormalisationType::Album => [data.album_gain_db, data.album_peak], - NormalisationType::Track => [data.track_gain_db, data.track_peak], + let [gain_db, gain_peak] = if config.normalisation_type == NormalisationType::Album { + [data.album_gain_db, data.album_peak] + } else { + [data.track_gain_db, data.track_peak] }; let normalisation_power = gain_db as f64 + config.normalisation_pregain; @@ -264,7 +268,11 @@ impl NormalisationData { } debug!("Normalisation Data: {:?}", data); - debug!("Normalisation Factor: {:.2}%", normalisation_factor * 100.0); + debug!( + "Calculated Normalisation Factor for {:?}: {:.2}%", + config.normalisation_type, + normalisation_factor * 100.0 + ); normalisation_factor as f64 } @@ -327,6 +335,8 @@ impl Player { limiter_peak_sample: 0.0, limiter_factor: 1.0, limiter_strength: 0.0, + + auto_normalise_as_album: false, }; // While PlayerInternal is written as a future, it still contains blocking code. @@ -406,6 +416,10 @@ impl Player { pub fn emit_volume_set_event(&self, volume: u16) { self.command(PlayerCommand::EmitVolumeSetEvent(volume)); } + + pub fn set_auto_normalise_as_album(&self, setting: bool) { + self.command(PlayerCommand::SetAutoNormaliseAsAlbum(setting)); + } } impl Drop for Player { @@ -423,7 +437,7 @@ impl Drop for Player { struct PlayerLoadedTrackData { decoder: Decoder, - normalisation_factor: f64, + normalisation_data: NormalisationData, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -456,6 +470,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, + normalisation_data: NormalisationData, normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, @@ -467,6 +482,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, + normalisation_data: NormalisationData, normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, @@ -543,7 +559,7 @@ impl PlayerState { decoder, duration_ms, bytes_per_second, - normalisation_factor, + normalisation_data, stream_loader_controller, stream_position_pcm, .. @@ -553,7 +569,7 @@ impl PlayerState { play_request_id, loaded_track: PlayerLoadedTrackData { decoder, - normalisation_factor, + normalisation_data, stream_loader_controller, bytes_per_second, duration_ms, @@ -572,6 +588,7 @@ impl PlayerState { track_id, play_request_id, decoder, + normalisation_data, normalisation_factor, stream_loader_controller, duration_ms, @@ -583,6 +600,7 @@ impl PlayerState { track_id, play_request_id, decoder, + normalisation_data, normalisation_factor, stream_loader_controller, duration_ms, @@ -603,6 +621,7 @@ impl PlayerState { track_id, play_request_id, decoder, + normalisation_data, normalisation_factor, stream_loader_controller, duration_ms, @@ -615,6 +634,7 @@ impl PlayerState { track_id, play_request_id, decoder, + normalisation_data, normalisation_factor, stream_loader_controller, duration_ms, @@ -775,14 +795,16 @@ impl PlayerTrackLoader { let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); - let normalisation_factor = match NormalisationData::parse_from_file(&mut decrypted_file) - { - Ok(normalisation_data) => { - NormalisationData::get_factor(&self.config, normalisation_data) - } + let normalisation_data = match NormalisationData::parse_from_file(&mut decrypted_file) { + Ok(data) => data, Err(_) => { warn!("Unable to extract normalisation data, using default value."); - 1.0 + NormalisationData { + track_gain_db: 0.0, + track_peak: 1.0, + album_gain_db: 0.0, + album_peak: 1.0, + } } }; @@ -838,7 +860,7 @@ impl PlayerTrackLoader { return Some(PlayerLoadedTrackData { decoder, - normalisation_factor, + normalisation_data, stream_loader_controller, bytes_per_second, duration_ms, @@ -1339,6 +1361,17 @@ impl PlayerInternal { ) { let position_ms = Self::position_pcm_to_ms(loaded_track.stream_position_pcm); + let mut config = self.config.clone(); + if config.normalisation_type == NormalisationType::Auto { + if self.auto_normalise_as_album { + config.normalisation_type = NormalisationType::Album; + } else { + config.normalisation_type = NormalisationType::Track; + } + }; + let normalisation_factor = + NormalisationData::get_factor(&config, loaded_track.normalisation_data); + if start_playback { self.ensure_sink_running(); @@ -1353,7 +1386,8 @@ impl PlayerInternal { track_id, play_request_id, decoder: loaded_track.decoder, - normalisation_factor: loaded_track.normalisation_factor, + normalisation_data: loaded_track.normalisation_data, + normalisation_factor, stream_loader_controller: loaded_track.stream_loader_controller, duration_ms: loaded_track.duration_ms, bytes_per_second: loaded_track.bytes_per_second, @@ -1370,7 +1404,8 @@ impl PlayerInternal { track_id, play_request_id, decoder: loaded_track.decoder, - normalisation_factor: loaded_track.normalisation_factor, + normalisation_data: loaded_track.normalisation_data, + normalisation_factor, stream_loader_controller: loaded_track.stream_loader_controller, duration_ms: loaded_track.duration_ms, bytes_per_second: loaded_track.bytes_per_second, @@ -1497,7 +1532,7 @@ impl PlayerInternal { stream_loader_controller, bytes_per_second, duration_ms, - normalisation_factor, + normalisation_data, .. } | PlayerState::Paused { @@ -1506,13 +1541,13 @@ impl PlayerInternal { stream_loader_controller, bytes_per_second, duration_ms, - normalisation_factor, + normalisation_data, .. } = old_state { let loaded_track = PlayerLoadedTrackData { decoder, - normalisation_factor, + normalisation_data, stream_loader_controller, bytes_per_second, duration_ms, @@ -1750,6 +1785,10 @@ impl PlayerInternal { PlayerCommand::EmitVolumeSetEvent(volume) => { self.send_event(PlayerEvent::VolumeSet { volume }) } + + PlayerCommand::SetAutoNormaliseAsAlbum(setting) => { + self.auto_normalise_as_album = setting + } } } @@ -1855,6 +1894,10 @@ impl ::std::fmt::Debug for PlayerCommand { PlayerCommand::EmitVolumeSetEvent(volume) => { f.debug_tuple("VolumeSet").field(&volume).finish() } + PlayerCommand::SetAutoNormaliseAsAlbum(setting) => f + .debug_tuple("SetAutoNormaliseAsAlbum") + .field(&setting) + .finish(), } } } diff --git a/src/main.rs b/src/main.rs index c896a303..76e8ba1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -359,7 +359,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", NORMALISATION_GAIN_TYPE, - "Specify the normalisation gain type to use {track|album}. Defaults to album.", + "Specify the normalisation gain type to use {track|album|auto}. Defaults to auto.", "TYPE", ) .optopt( From 89577d1fc130805695910b1330af2ce6dc89fa02 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 20 Sep 2021 12:29:12 -0500 Subject: [PATCH 20/95] Improve player (#823) * Improve error handling * Harmonize `Seek`: Make the decoders and player use the same math for converting between samples and milliseconds * Reduce duplicate calls: Make decoder seek in PCM, not ms * Simplify decoder errors with `thiserror` --- CHANGELOG.md | 2 +- playback/Cargo.toml | 10 +- playback/src/audio_backend/jackaudio.rs | 5 +- playback/src/audio_backend/portaudio.rs | 5 +- playback/src/audio_backend/rodio.rs | 4 +- playback/src/audio_backend/sdl.rs | 4 +- playback/src/decoder/lewton_decoder.rs | 58 ++--- playback/src/decoder/mod.rs | 69 +++-- playback/src/decoder/passthrough_decoder.rs | 89 +++---- playback/src/lib.rs | 2 + playback/src/player.rs | 274 +++++++++++++------- 11 files changed, 280 insertions(+), 242 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6235b017..8a056bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] Add `--normalisation-type auto` that switches between album and track automatically ### Changed -- [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) +- [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `DecoderError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) - [audio, playback] Use `Duration` for time constants and functions (breaking) - [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate - [connect] Synchronize player volume with mixer volume on playback diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 8211f2bd..f2fdaf48 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -25,6 +25,7 @@ byteorder = "1.4" shell-words = "1.0.0" tokio = { version = "1", features = ["sync"] } zerocopy = { version = "0.3" } +thiserror = { version = "1" } # Backends alsa = { version = "0.5", optional = true } @@ -40,7 +41,6 @@ glib = { version = "0.10", optional = true } # Rodio dependencies rodio = { version = "0.14", optional = true, default-features = false } cpal = { version = "0.13", optional = true } -thiserror = { version = "1", optional = true } # Decoder lewton = "0.10" @@ -51,11 +51,11 @@ rand = "0.8" rand_distr = "0.4" [features] -alsa-backend = ["alsa", "thiserror"] +alsa-backend = ["alsa"] portaudio-backend = ["portaudio-rs"] -pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding", "thiserror"] +pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] jackaudio-backend = ["jack"] -rodio-backend = ["rodio", "cpal", "thiserror"] -rodiojack-backend = ["rodio", "cpal/jack", "thiserror"] +rodio-backend = ["rodio", "cpal"] +rodiojack-backend = ["rodio", "cpal/jack"] sdl-backend = ["sdl2"] gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index f55f20a8..a8f37524 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -71,7 +71,10 @@ impl Open for JackSink { impl Sink for JackSink { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { - let samples_f32: &[f32] = &converter.f64_to_f32(packet.samples()); + let samples = packet + .samples() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + let samples_f32: &[f32] = &converter.f64_to_f32(samples); for sample in samples_f32.iter() { let res = self.send.send(*sample); if res.is_err() { diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 378deb48..26355a03 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -148,7 +148,10 @@ impl<'a> Sink for PortAudioSink<'a> { }; } - let samples = packet.samples(); + let samples = packet + .samples() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + let result = match self { Self::F32(stream, _parameters) => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 1e999938..4d9c65c5 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -176,7 +176,9 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro impl Sink for RodioSink { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { - let samples = packet.samples(); + let samples = packet + .samples() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; match self.format { AudioFormat::F32 => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 28d140e8..63a88c22 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -92,7 +92,9 @@ impl Sink for SdlSink { }}; } - let samples = packet.samples(); + let samples = packet + .samples() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; match self { Self::F32(queue) => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); diff --git a/playback/src/decoder/lewton_decoder.rs b/playback/src/decoder/lewton_decoder.rs index adf63e2a..bc90b992 100644 --- a/playback/src/decoder/lewton_decoder.rs +++ b/playback/src/decoder/lewton_decoder.rs @@ -1,22 +1,23 @@ -use super::{AudioDecoder, AudioError, AudioPacket}; +use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; +use lewton::audio::AudioReadError::AudioIsHeader; use lewton::inside_ogg::OggStreamReader; use lewton::samples::InterleavedSamples; +use lewton::OggReadError::NoCapturePatternFound; +use lewton::VorbisError::{BadAudio, OggError}; -use std::error; -use std::fmt; use std::io::{Read, Seek}; -use std::time::Duration; pub struct VorbisDecoder(OggStreamReader); -pub struct VorbisError(lewton::VorbisError); impl VorbisDecoder where R: Read + Seek, { - pub fn new(input: R) -> Result, VorbisError> { - Ok(VorbisDecoder(OggStreamReader::new(input)?)) + pub fn new(input: R) -> DecoderResult> { + let reader = + OggStreamReader::new(input).map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?; + Ok(VorbisDecoder(reader)) } } @@ -24,51 +25,22 @@ impl AudioDecoder for VorbisDecoder where R: Read + Seek, { - fn seek(&mut self, ms: i64) -> Result<(), AudioError> { - let absgp = Duration::from_millis(ms as u64 * crate::SAMPLE_RATE as u64).as_secs(); - match self.0.seek_absgp_pg(absgp as u64) { - Ok(_) => Ok(()), - Err(err) => Err(AudioError::VorbisError(err.into())), - } + fn seek(&mut self, absgp: u64) -> DecoderResult<()> { + self.0 + .seek_absgp_pg(absgp) + .map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?; + Ok(()) } - fn next_packet(&mut self) -> Result, AudioError> { - use lewton::audio::AudioReadError::AudioIsHeader; - use lewton::OggReadError::NoCapturePatternFound; - use lewton::VorbisError::{BadAudio, OggError}; + fn next_packet(&mut self) -> DecoderResult> { loop { match self.0.read_dec_packet_generic::>() { Ok(Some(packet)) => return Ok(Some(AudioPacket::samples_from_f32(packet.samples))), Ok(None) => return Ok(None), - Err(BadAudio(AudioIsHeader)) => (), Err(OggError(NoCapturePatternFound)) => (), - Err(err) => return Err(AudioError::VorbisError(err.into())), + Err(e) => return Err(DecoderError::LewtonDecoder(e.to_string())), } } } } - -impl From for VorbisError { - fn from(err: lewton::VorbisError) -> VorbisError { - VorbisError(err) - } -} - -impl fmt::Debug for VorbisError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } -} - -impl fmt::Display for VorbisError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - -impl error::Error for VorbisError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - error::Error::source(&self.0) - } -} diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index 9641e8b3..087bba4c 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -1,10 +1,30 @@ -use std::fmt; +use thiserror::Error; mod lewton_decoder; -pub use lewton_decoder::{VorbisDecoder, VorbisError}; +pub use lewton_decoder::VorbisDecoder; mod passthrough_decoder; -pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; +pub use passthrough_decoder::PassthroughDecoder; + +#[derive(Error, Debug)] +pub enum DecoderError { + #[error("Lewton Decoder Error: {0}")] + LewtonDecoder(String), + #[error("Passthrough Decoder Error: {0}")] + PassthroughDecoder(String), +} + +pub type DecoderResult = Result; + +#[derive(Error, Debug)] +pub enum AudioPacketError { + #[error("Decoder OggData Error: Can't return OggData on Samples")] + OggData, + #[error("Decoder Samples Error: Can't return Samples on OggData")] + Samples, +} + +pub type AudioPacketResult = Result; pub enum AudioPacket { Samples(Vec), @@ -17,17 +37,17 @@ impl AudioPacket { AudioPacket::Samples(f64_samples) } - pub fn samples(&self) -> &[f64] { + pub fn samples(&self) -> AudioPacketResult<&[f64]> { match self { - AudioPacket::Samples(s) => s, - AudioPacket::OggData(_) => panic!("can't return OggData on samples"), + AudioPacket::Samples(s) => Ok(s), + AudioPacket::OggData(_) => Err(AudioPacketError::OggData), } } - pub fn oggdata(&self) -> &[u8] { + pub fn oggdata(&self) -> AudioPacketResult<&[u8]> { match self { - AudioPacket::Samples(_) => panic!("can't return samples on OggData"), - AudioPacket::OggData(d) => d, + AudioPacket::OggData(d) => Ok(d), + AudioPacket::Samples(_) => Err(AudioPacketError::Samples), } } @@ -39,34 +59,7 @@ impl AudioPacket { } } -#[derive(Debug)] -pub enum AudioError { - PassthroughError(PassthroughError), - VorbisError(VorbisError), -} - -impl fmt::Display for AudioError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - AudioError::PassthroughError(err) => write!(f, "PassthroughError({})", err), - AudioError::VorbisError(err) => write!(f, "VorbisError({})", err), - } - } -} - -impl From for AudioError { - fn from(err: VorbisError) -> AudioError { - AudioError::VorbisError(err) - } -} - -impl From for AudioError { - fn from(err: PassthroughError) -> AudioError { - AudioError::PassthroughError(err) - } -} - pub trait AudioDecoder { - fn seek(&mut self, ms: i64) -> Result<(), AudioError>; - fn next_packet(&mut self) -> Result, AudioError>; + fn seek(&mut self, absgp: u64) -> DecoderResult<()>; + fn next_packet(&mut self) -> DecoderResult>; } diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index 7c1ad532..dd8e3b32 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -1,23 +1,22 @@ // Passthrough decoder for librespot -use super::{AudioDecoder, AudioError, AudioPacket}; -use crate::SAMPLE_RATE; +use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; -use std::fmt; use std::io::{Read, Seek}; -use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; -fn get_header(code: u8, rdr: &mut PacketReader) -> Result, PassthroughError> +fn get_header(code: u8, rdr: &mut PacketReader) -> DecoderResult> where T: Read + Seek, { - let pck: Packet = rdr.read_packet_expected()?; + let pck: Packet = rdr + .read_packet_expected() + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; let pkt_type = pck.data[0]; debug!("Vorbis header type {}", &pkt_type); if pkt_type != code { - return Err(PassthroughError(OggReadError::InvalidData)); + return Err(DecoderError::PassthroughDecoder("Invalid Data".to_string())); } Ok(pck.data.into_boxed_slice()) @@ -35,16 +34,14 @@ pub struct PassthroughDecoder { setup: Box<[u8]>, } -pub struct PassthroughError(ogg::OggReadError); - impl PassthroughDecoder { /// Constructs a new Decoder from a given implementation of `Read + Seek`. - pub fn new(rdr: R) -> Result { + pub fn new(rdr: R) -> DecoderResult { let mut rdr = PacketReader::new(rdr); - let stream_serial = SystemTime::now() + let since_epoch = SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u32; + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; + let stream_serial = since_epoch.as_millis() as u32; info!("Starting passthrough track with serial {}", stream_serial); @@ -71,9 +68,7 @@ impl PassthroughDecoder { } impl AudioDecoder for PassthroughDecoder { - fn seek(&mut self, ms: i64) -> Result<(), AudioError> { - info!("Seeking to {}", ms); - + fn seek(&mut self, absgp: u64) -> DecoderResult<()> { // add an eos to previous stream if missing if self.bos && !self.eos { match self.rdr.read_packet() { @@ -86,7 +81,7 @@ impl AudioDecoder for PassthroughDecoder { PacketWriteEndInfo::EndStream, absgp_page, ) - .unwrap(); + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; } _ => warn! {"Cannot write EoS after seeking"}, }; @@ -97,23 +92,29 @@ impl AudioDecoder for PassthroughDecoder { self.ofsgp_page = 0; self.stream_serial += 1; - // hard-coded to 44.1 kHz - match self.rdr.seek_absgp( - None, - Duration::from_millis(ms as u64 * SAMPLE_RATE as u64).as_secs(), - ) { + match self.rdr.seek_absgp(None, absgp) { Ok(_) => { // need to set some offset for next_page() - let pck = self.rdr.read_packet().unwrap().unwrap(); - self.ofsgp_page = pck.absgp_page(); - debug!("Seek to offset page {}", self.ofsgp_page); - Ok(()) + let pck = self + .rdr + .read_packet() + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; + match pck { + Some(pck) => { + self.ofsgp_page = pck.absgp_page(); + debug!("Seek to offset page {}", self.ofsgp_page); + Ok(()) + } + None => Err(DecoderError::PassthroughDecoder( + "Packet is None".to_string(), + )), + } } - Err(err) => Err(AudioError::PassthroughError(err.into())), + Err(e) => Err(DecoderError::PassthroughDecoder(e.to_string())), } } - fn next_packet(&mut self) -> Result, AudioError> { + fn next_packet(&mut self) -> DecoderResult> { // write headers if we are (re)starting if !self.bos { self.wtr @@ -123,7 +124,7 @@ impl AudioDecoder for PassthroughDecoder { PacketWriteEndInfo::EndPage, 0, ) - .unwrap(); + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; self.wtr .write_packet( self.comment.clone(), @@ -131,7 +132,7 @@ impl AudioDecoder for PassthroughDecoder { PacketWriteEndInfo::NormalPacket, 0, ) - .unwrap(); + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; self.wtr .write_packet( self.setup.clone(), @@ -139,7 +140,7 @@ impl AudioDecoder for PassthroughDecoder { PacketWriteEndInfo::EndPage, 0, ) - .unwrap(); + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; self.bos = true; debug!("Wrote Ogg headers"); } @@ -151,7 +152,7 @@ impl AudioDecoder for PassthroughDecoder { info!("end of streaming"); return Ok(None); } - Err(err) => return Err(AudioError::PassthroughError(err.into())), + Err(e) => return Err(DecoderError::PassthroughDecoder(e.to_string())), }; let pckgp_page = pck.absgp_page(); @@ -178,32 +179,14 @@ impl AudioDecoder for PassthroughDecoder { inf, pckgp_page - self.ofsgp_page, ) - .unwrap(); + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; let data = self.wtr.inner_mut(); if !data.is_empty() { - let result = AudioPacket::OggData(std::mem::take(data)); - return Ok(Some(result)); + let ogg_data = AudioPacket::OggData(std::mem::take(data)); + return Ok(Some(ogg_data)); } } } } - -impl fmt::Debug for PassthroughError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } -} - -impl From for PassthroughError { - fn from(err: OggReadError) -> PassthroughError { - PassthroughError(err) - } -} - -impl fmt::Display for PassthroughError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} diff --git a/playback/src/lib.rs b/playback/src/lib.rs index e39dfc7c..a52ca2fa 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -16,3 +16,5 @@ pub mod player; pub const SAMPLE_RATE: u32 = 44100; pub const NUM_CHANNELS: u8 = 2; pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; +pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0; +pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64; diff --git a/playback/src/player.rs b/playback/src/player.rs index d858e333..a7ff916d 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -23,11 +23,11 @@ use crate::convert::Converter; use crate::core::session::Session; use crate::core::spotify_id::SpotifyId; use crate::core::util::SeqGenerator; -use crate::decoder::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder}; +use crate::decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder}; use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; -use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND}; +use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND}; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; pub const DB_VOLTAGE_RATIO: f64 = 20.0; @@ -356,7 +356,11 @@ impl Player { } fn command(&self, cmd: PlayerCommand) { - self.commands.as_ref().unwrap().send(cmd).unwrap(); + if let Some(commands) = self.commands.as_ref() { + if let Err(e) = commands.send(cmd) { + error!("Player Commands Error: {}", e); + } + } } pub fn load(&mut self, track_id: SpotifyId, start_playing: bool, position_ms: u32) -> u64 { @@ -429,7 +433,7 @@ impl Drop for Player { if let Some(handle) = self.thread_handle.take() { match handle.join() { Ok(_) => (), - Err(_) => error!("Player thread panicked!"), + Err(e) => error!("Player thread Error: {:?}", e), } } } @@ -505,7 +509,10 @@ impl PlayerState { match *self { Stopped | EndOfTrack { .. } | Paused { .. } | Loading { .. } => false, Playing { .. } => true, - Invalid => panic!("invalid state"), + Invalid => { + error!("PlayerState is_playing: invalid state"); + exit(1); + } } } @@ -530,7 +537,10 @@ impl PlayerState { | Playing { ref mut decoder, .. } => Some(decoder), - Invalid => panic!("invalid state"), + Invalid => { + error!("PlayerState decoder: invalid state"); + exit(1); + } } } @@ -546,7 +556,10 @@ impl PlayerState { ref mut stream_loader_controller, .. } => Some(stream_loader_controller), - Invalid => panic!("invalid state"), + Invalid => { + error!("PlayerState stream_loader_controller: invalid state"); + exit(1); + } } } @@ -577,7 +590,10 @@ impl PlayerState { }, }; } - _ => panic!("Called playing_to_end_of_track in non-playing state."), + _ => { + error!("Called playing_to_end_of_track in non-playing state."); + exit(1); + } } } @@ -610,7 +626,10 @@ impl PlayerState { suggested_to_preload_next_track, }; } - _ => panic!("invalid state"), + _ => { + error!("PlayerState paused_to_playing: invalid state"); + exit(1); + } } } @@ -643,7 +662,10 @@ impl PlayerState { suggested_to_preload_next_track, }; } - _ => panic!("invalid state"), + _ => { + error!("PlayerState playing_to_paused: invalid state"); + exit(1); + } } } } @@ -699,8 +721,8 @@ impl PlayerTrackLoader { ) -> Option { let audio = match AudioItem::get_audio_item(&self.session, spotify_id).await { Ok(audio) => audio, - Err(_) => { - error!("Unable to load audio item."); + Err(e) => { + error!("Unable to load audio item: {:?}", e); return None; } }; @@ -768,8 +790,8 @@ impl PlayerTrackLoader { let encrypted_file = match encrypted_file.await { Ok(encrypted_file) => encrypted_file, - Err(_) => { - error!("Unable to load encrypted file."); + Err(e) => { + error!("Unable to load encrypted file: {:?}", e); return None; } }; @@ -787,8 +809,8 @@ impl PlayerTrackLoader { let key = match self.session.audio_key().request(spotify_id, file_id).await { Ok(key) => key, - Err(_) => { - error!("Unable to load decryption key"); + Err(e) => { + error!("Unable to load decryption key: {:?}", e); return None; } }; @@ -813,12 +835,12 @@ impl PlayerTrackLoader { let result = if self.config.passthrough { match PassthroughDecoder::new(audio_file) { Ok(result) => Ok(Box::new(result) as Decoder), - Err(e) => Err(AudioError::PassthroughError(e)), + Err(e) => Err(DecoderError::PassthroughDecoder(e.to_string())), } } else { match VorbisDecoder::new(audio_file) { Ok(result) => Ok(Box::new(result) as Decoder), - Err(e) => Err(AudioError::VorbisError(e)), + Err(e) => Err(DecoderError::LewtonDecoder(e.to_string())), } }; @@ -830,14 +852,17 @@ impl PlayerTrackLoader { e ); - if self - .session - .cache() - .expect("If the audio file is cached, a cache should exist") - .remove_file(file_id) - .is_err() - { - return None; + match self.session.cache() { + Some(cache) => { + if cache.remove_file(file_id).is_err() { + error!("Error removing file from cache"); + return None; + } + } + None => { + error!("If the audio file is cached, a cache should exist"); + return None; + } } // Just try it again @@ -849,13 +874,15 @@ impl PlayerTrackLoader { } }; - if position_ms != 0 { - if let Err(err) = decoder.seek(position_ms as i64) { - error!("Vorbis error: {}", err); + let position_pcm = PlayerInternal::position_ms_to_pcm(position_ms); + + if position_pcm != 0 { + if let Err(e) = decoder.seek(position_pcm) { + error!("PlayerTrackLoader load_track: {}", e); } stream_loader_controller.set_stream_mode(); } - let stream_position_pcm = PlayerInternal::position_ms_to_pcm(position_ms); + let stream_position_pcm = position_pcm; info!("<{}> ({} ms) loaded", audio.name, audio.duration); return Some(PlayerLoadedTrackData { @@ -912,7 +939,8 @@ impl Future for PlayerInternal { start_playback, ); if let PlayerState::Loading { .. } = self.state { - panic!("The state wasn't changed by start_playback()"); + error!("The state wasn't changed by start_playback()"); + exit(1); } } Poll::Ready(Err(_)) => { @@ -976,47 +1004,67 @@ impl Future for PlayerInternal { .. } = self.state { - let packet = decoder.next_packet().expect("Vorbis error"); + match decoder.next_packet() { + Ok(packet) => { + if !passthrough { + if let Some(ref packet) = packet { + match packet.samples() { + Ok(samples) => { + *stream_position_pcm += + (samples.len() / NUM_CHANNELS as usize) as u64; + let stream_position_millis = + Self::position_pcm_to_ms(*stream_position_pcm); - if !passthrough { - if let Some(ref packet) = packet { - *stream_position_pcm += - (packet.samples().len() / NUM_CHANNELS as usize) as u64; - let stream_position_millis = - Self::position_pcm_to_ms(*stream_position_pcm); - - let notify_about_position = match *reported_nominal_start_time { - None => true, - Some(reported_nominal_start_time) => { - // only notify if we're behind. If we're ahead it's probably due to a buffer of the backend and we're actually in time. - let lag = (Instant::now() - reported_nominal_start_time) - .as_millis() - as i64 - - stream_position_millis as i64; - lag > Duration::from_secs(1).as_millis() as i64 + let notify_about_position = + match *reported_nominal_start_time { + None => true, + Some(reported_nominal_start_time) => { + // only notify if we're behind. If we're ahead it's probably due to a buffer of the backend and we're actually in time. + let lag = (Instant::now() + - reported_nominal_start_time) + .as_millis() + as i64 + - stream_position_millis as i64; + lag > Duration::from_secs(1).as_millis() + as i64 + } + }; + if notify_about_position { + *reported_nominal_start_time = Some( + Instant::now() + - Duration::from_millis( + stream_position_millis as u64, + ), + ); + self.send_event(PlayerEvent::Playing { + track_id, + play_request_id, + position_ms: stream_position_millis as u32, + duration_ms, + }); + } + } + Err(e) => { + error!("PlayerInternal poll: {}", e); + exit(1); + } + } } - }; - if notify_about_position { - *reported_nominal_start_time = Some( - Instant::now() - - Duration::from_millis(stream_position_millis as u64), - ); - self.send_event(PlayerEvent::Playing { - track_id, - play_request_id, - position_ms: stream_position_millis as u32, - duration_ms, - }); + } else { + // position, even if irrelevant, must be set so that seek() is called + *stream_position_pcm = duration_ms.into(); } - } - } else { - // position, even if irrelevant, must be set so that seek() is called - *stream_position_pcm = duration_ms.into(); - } - self.handle_packet(packet, normalisation_factor); + self.handle_packet(packet, normalisation_factor); + } + Err(e) => { + error!("PlayerInternal poll: {}", e); + exit(1); + } + } } else { - unreachable!(); + error!("PlayerInternal poll: Invalid PlayerState"); + exit(1); }; } @@ -1065,11 +1113,11 @@ impl Future for PlayerInternal { impl PlayerInternal { fn position_pcm_to_ms(position_pcm: u64) -> u32 { - (position_pcm * 10 / 441) as u32 + (position_pcm as f64 * MS_PER_PAGE) as u32 } fn position_ms_to_pcm(position_ms: u32) -> u64 { - position_ms as u64 * 441 / 10 + (position_ms as f64 * PAGES_PER_MS) as u64 } fn ensure_sink_running(&mut self) { @@ -1080,8 +1128,8 @@ impl PlayerInternal { } match self.sink.start() { Ok(()) => self.sink_status = SinkStatus::Running, - Err(err) => { - error!("Fatal error, could not start audio sink: {}", err); + Err(e) => { + error!("{}", e); exit(1); } } @@ -1103,8 +1151,8 @@ impl PlayerInternal { callback(self.sink_status); } } - Err(err) => { - error!("Fatal error, could not stop audio sink: {}", err); + Err(e) => { + error!("{}", e); exit(1); } } @@ -1151,7 +1199,10 @@ impl PlayerInternal { self.state = PlayerState::Stopped; } PlayerState::Stopped => (), - PlayerState::Invalid => panic!("invalid state"), + PlayerState::Invalid => { + error!("PlayerInternal handle_player_stop: invalid state"); + exit(1); + } } } @@ -1317,8 +1368,8 @@ impl PlayerInternal { } } - if let Err(err) = self.sink.write(&packet, &mut self.converter) { - error!("Fatal error, could not write audio to audio sink: {}", err); + if let Err(e) = self.sink.write(&packet, &mut self.converter) { + error!("{}", e); exit(1); } } @@ -1337,7 +1388,8 @@ impl PlayerInternal { play_request_id, }) } else { - unreachable!(); + error!("PlayerInternal handle_packet: Invalid PlayerState"); + exit(1); } } } @@ -1458,7 +1510,10 @@ impl PlayerInternal { play_request_id, position_ms, }), - PlayerState::Invalid { .. } => panic!("Player is in an invalid state."), + PlayerState::Invalid { .. } => { + error!("PlayerInternal handle_command_load: invalid state"); + exit(1); + } } // Now we check at different positions whether we already have a pre-loaded version @@ -1474,24 +1529,30 @@ impl PlayerInternal { if previous_track_id == track_id { let mut loaded_track = match mem::replace(&mut self.state, PlayerState::Invalid) { PlayerState::EndOfTrack { loaded_track, .. } => loaded_track, - _ => unreachable!(), + _ => { + error!("PlayerInternal handle_command_load: Invalid PlayerState"); + exit(1); + } }; - if Self::position_ms_to_pcm(position_ms) != loaded_track.stream_position_pcm { + let position_pcm = Self::position_ms_to_pcm(position_ms); + + if position_pcm != loaded_track.stream_position_pcm { loaded_track .stream_loader_controller .set_random_access_mode(); - let _ = loaded_track.decoder.seek(position_ms as i64); // This may be blocking. - // But most likely the track is fully - // loaded already because we played - // to the end of it. + if let Err(e) = loaded_track.decoder.seek(position_pcm) { + // This may be blocking. + error!("PlayerInternal handle_command_load: {}", e); + } loaded_track.stream_loader_controller.set_stream_mode(); - loaded_track.stream_position_pcm = Self::position_ms_to_pcm(position_ms); + loaded_track.stream_position_pcm = position_pcm; } self.preload = PlayerPreload::None; self.start_playback(track_id, play_request_id, loaded_track, play); if let PlayerState::Invalid = self.state { - panic!("start_playback() hasn't set a valid player state."); + error!("start_playback() hasn't set a valid player state."); + exit(1); } return; } @@ -1515,11 +1576,16 @@ impl PlayerInternal { { if current_track_id == track_id { // we can use the current decoder. Ensure it's at the correct position. - if Self::position_ms_to_pcm(position_ms) != *stream_position_pcm { + let position_pcm = Self::position_ms_to_pcm(position_ms); + + if position_pcm != *stream_position_pcm { stream_loader_controller.set_random_access_mode(); - let _ = decoder.seek(position_ms as i64); // This may be blocking. + if let Err(e) = decoder.seek(position_pcm) { + // This may be blocking. + error!("PlayerInternal handle_command_load: {}", e); + } stream_loader_controller.set_stream_mode(); - *stream_position_pcm = Self::position_ms_to_pcm(position_ms); + *stream_position_pcm = position_pcm; } // Move the info from the current state into a PlayerLoadedTrackData so we can use @@ -1558,12 +1624,14 @@ impl PlayerInternal { self.start_playback(track_id, play_request_id, loaded_track, play); if let PlayerState::Invalid = self.state { - panic!("start_playback() hasn't set a valid player state."); + error!("start_playback() hasn't set a valid player state."); + exit(1); } return; } else { - unreachable!(); + error!("PlayerInternal handle_command_load: Invalid PlayerState"); + exit(1); } } } @@ -1581,17 +1649,23 @@ impl PlayerInternal { mut loaded_track, } = preload { - if Self::position_ms_to_pcm(position_ms) != loaded_track.stream_position_pcm { + let position_pcm = Self::position_ms_to_pcm(position_ms); + + if position_pcm != loaded_track.stream_position_pcm { loaded_track .stream_loader_controller .set_random_access_mode(); - let _ = loaded_track.decoder.seek(position_ms as i64); // This may be blocking + if let Err(e) = loaded_track.decoder.seek(position_pcm) { + // This may be blocking + error!("PlayerInternal handle_command_load: {}", e); + } loaded_track.stream_loader_controller.set_stream_mode(); } self.start_playback(track_id, play_request_id, *loaded_track, play); return; } else { - unreachable!(); + error!("PlayerInternal handle_command_load: Invalid PlayerState"); + exit(1); } } } @@ -1697,7 +1771,9 @@ impl PlayerInternal { stream_loader_controller.set_random_access_mode(); } if let Some(decoder) = self.state.decoder() { - match decoder.seek(position_ms as i64) { + let position_pcm = Self::position_ms_to_pcm(position_ms); + + match decoder.seek(position_pcm) { Ok(_) => { if let PlayerState::Playing { ref mut stream_position_pcm, @@ -1708,10 +1784,10 @@ impl PlayerInternal { .. } = self.state { - *stream_position_pcm = Self::position_ms_to_pcm(position_ms); + *stream_position_pcm = position_pcm; } } - Err(err) => error!("Vorbis error: {:?}", err), + Err(e) => error!("PlayerInternal handle_command_seek: {}", e), } } else { warn!("Player::seek called from invalid state"); @@ -1954,7 +2030,9 @@ struct Subfile { impl Subfile { pub fn new(mut stream: T, offset: u64) -> Subfile { - stream.seek(SeekFrom::Start(offset)).unwrap(); + if let Err(e) = stream.seek(SeekFrom::Start(offset)) { + error!("Subfile new Error: {}", e); + } Subfile { stream, offset } } } From de177f1260d839b5b49808ba1115b515e3781772 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 20 Sep 2021 20:12:57 +0200 Subject: [PATCH 21/95] Update num-bigint --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d631c83..e94d21b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1508,9 +1508,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" +checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535" dependencies = [ "autocfg", "num-integer", From 8d70fd910eda39a7a927ddcf26579d4cbb9188ae Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 27 Sep 2021 13:46:26 -0500 Subject: [PATCH 22/95] Implement common SinkError and SinkResult (#820) * Make error messages more consistent and concise. * `impl From for io::Error` so `AlsaErrors` can be thrown to player as `io::Errors`. This little bit of boilerplate goes a long way to simplifying things further down in the code. And will make any needed future changes easier. * Bonus: handle ALSA backend buffer sizing a little better. --- playback/src/audio_backend/alsa.rs | 199 ++++++++++++----------- playback/src/audio_backend/gstreamer.rs | 6 +- playback/src/audio_backend/jackaudio.rs | 8 +- playback/src/audio_backend/mod.rs | 26 ++- playback/src/audio_backend/pipe.rs | 18 +- playback/src/audio_backend/portaudio.rs | 11 +- playback/src/audio_backend/pulseaudio.rs | 140 ++++++++-------- playback/src/audio_backend/rodio.rs | 32 +++- playback/src/audio_backend/sdl.rs | 12 +- playback/src/audio_backend/subprocess.rs | 43 +++-- 10 files changed, 275 insertions(+), 220 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 8b8962fb..17798868 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,4 +1,4 @@ -use super::{Open, Sink, SinkAsBytes}; +use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; @@ -7,7 +7,6 @@ use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, HwParams, PCM}; use alsa::{Direction, ValueOr}; use std::cmp::min; -use std::io; use std::process::exit; use std::time::Duration; use thiserror::Error; @@ -18,34 +17,67 @@ const BUFFER_TIME: Duration = Duration::from_millis(500); #[derive(Debug, Error)] enum AlsaError { - #[error("AlsaSink, device {device} may be invalid or busy, {err}")] - PcmSetUp { device: String, err: alsa::Error }, - #[error("AlsaSink, device {device} unsupported access type RWInterleaved, {err}")] - UnsupportedAccessType { device: String, err: alsa::Error }, - #[error("AlsaSink, device {device} unsupported format {format:?}, {err}")] + #[error(" Device {device} Unsupported Format {alsa_format:?} ({format:?}), {e}")] UnsupportedFormat { device: String, + alsa_format: Format, format: AudioFormat, - err: alsa::Error, + e: alsa::Error, }, - #[error("AlsaSink, device {device} unsupported sample rate {samplerate}, {err}")] - UnsupportedSampleRate { - device: String, - samplerate: u32, - err: alsa::Error, - }, - #[error("AlsaSink, device {device} unsupported channel count {channel_count}, {err}")] + + #[error(" Device {device} Unsupported Channel Count {channel_count}, {e}")] UnsupportedChannelCount { device: String, channel_count: u8, - err: alsa::Error, + e: alsa::Error, }, - #[error("AlsaSink Hardware Parameters Error, {0}")] + + #[error(" Device {device} Unsupported Sample Rate {samplerate}, {e}")] + UnsupportedSampleRate { + device: String, + samplerate: u32, + e: alsa::Error, + }, + + #[error(" Device {device} Unsupported Access Type RWInterleaved, {e}")] + UnsupportedAccessType { device: String, e: alsa::Error }, + + #[error(" Device {device} May be Invalid, Busy, or Already in Use, {e}")] + PcmSetUp { device: String, e: alsa::Error }, + + #[error(" Failed to Drain PCM Buffer, {0}")] + DrainFailure(alsa::Error), + + #[error(" {0}")] + OnWrite(alsa::Error), + + #[error(" Hardware, {0}")] HwParams(alsa::Error), - #[error("AlsaSink Software Parameters Error, {0}")] + + #[error(" Software, {0}")] SwParams(alsa::Error), - #[error("AlsaSink PCM Error, {0}")] + + #[error(" PCM, {0}")] Pcm(alsa::Error), + + #[error(" Could Not Parse Ouput Name(s) and/or Description(s)")] + Parsing, + + #[error("")] + NotConnected, +} + +impl From for SinkError { + fn from(e: AlsaError) -> SinkError { + use AlsaError::*; + let es = e.to_string(); + match e { + DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es), + PcmSetUp { .. } => SinkError::ConnectionRefused(es), + NotConnected => SinkError::NotConnected(es), + _ => SinkError::InvalidParams(es), + } + } } pub struct AlsaSink { @@ -55,25 +87,19 @@ pub struct AlsaSink { period_buffer: Vec, } -fn list_outputs() -> io::Result<()> { +fn list_outputs() -> SinkResult<()> { println!("Listing available Alsa outputs:"); for t in &["pcm", "ctl", "hwdep"] { println!("{} devices:", t); - let i = match HintIter::new_str(None, t) { - Ok(i) => i, - Err(e) => { - return Err(io::Error::new(io::ErrorKind::Other, e)); - } - }; + + let i = HintIter::new_str(None, &t).map_err(|_| AlsaError::Parsing)?; + for a in i { if let Some(Direction::Playback) = a.direction { // mimic aplay -L - let name = a - .name - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not parse name"))?; - let desc = a - .desc - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not parse desc"))?; + let name = a.name.ok_or(AlsaError::Parsing)?; + let desc = a.desc.ok_or(AlsaError::Parsing)?; + println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t")); } } @@ -82,10 +108,10 @@ fn list_outputs() -> io::Result<()> { Ok(()) } -fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), AlsaError> { +fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> { let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp { device: dev_name.to_string(), - err: e, + e, })?; let alsa_format = match format { @@ -103,24 +129,26 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Alsa let bytes_per_period = { let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?; + hwp.set_access(Access::RWInterleaved) .map_err(|e| AlsaError::UnsupportedAccessType { device: dev_name.to_string(), - err: e, + e, })?; hwp.set_format(alsa_format) .map_err(|e| AlsaError::UnsupportedFormat { device: dev_name.to_string(), + alsa_format, format, - err: e, + e, })?; hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| { AlsaError::UnsupportedSampleRate { device: dev_name.to_string(), samplerate: SAMPLE_RATE, - err: e, + e, } })?; @@ -128,7 +156,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Alsa .map_err(|e| AlsaError::UnsupportedChannelCount { device: dev_name.to_string(), channel_count: NUM_CHANNELS, - err: e, + e, })?; hwp.set_buffer_time_near(BUFFER_TIME.as_micros() as u32, ValueOr::Nearest) @@ -141,8 +169,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Alsa let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; - // Don't assume we got what we wanted. - // Ask to make sure. + // Don't assume we got what we wanted. Ask to make sure. let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?; let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?; @@ -171,8 +198,8 @@ impl Open for AlsaSink { Ok(_) => { exit(0); } - Err(err) => { - error!("Error listing Alsa outputs, {}", err); + Err(e) => { + error!("{}", e); exit(1); } }, @@ -193,53 +220,40 @@ impl Open for AlsaSink { } impl Sink for AlsaSink { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { if self.pcm.is_none() { - match open_device(&self.device, self.format) { - Ok((pcm, bytes_per_period)) => { - self.pcm = Some(pcm); - // If the capacity is greater than we want shrink it - // to it's current len (which should be zero) before - // setting the capacity with reserve_exact. - if self.period_buffer.capacity() > bytes_per_period { - self.period_buffer.shrink_to_fit(); - } - // This does nothing if the capacity is already sufficient. - // Len should always be zero, but for the sake of being thorough... - self.period_buffer - .reserve_exact(bytes_per_period - self.period_buffer.len()); + let (pcm, bytes_per_period) = open_device(&self.device, self.format)?; + self.pcm = Some(pcm); - // Should always match the "Period Buffer size in bytes: " trace! message. - trace!( - "Period Buffer capacity: {:?}", - self.period_buffer.capacity() - ); - } - Err(e) => { - return Err(io::Error::new(io::ErrorKind::Other, e)); - } + let current_capacity = self.period_buffer.capacity(); + + if current_capacity > bytes_per_period { + self.period_buffer.truncate(bytes_per_period); + self.period_buffer.shrink_to_fit(); + } else if current_capacity < bytes_per_period { + let extra = bytes_per_period - self.period_buffer.len(); + self.period_buffer.reserve_exact(extra); } + + // Should always match the "Period Buffer size in bytes: " trace! message. + trace!( + "Period Buffer capacity: {:?}", + self.period_buffer.capacity() + ); } Ok(()) } - fn stop(&mut self) -> io::Result<()> { + fn stop(&mut self) -> SinkResult<()> { // Zero fill the remainder of the period buffer and // write any leftover data before draining the actual PCM buffer. self.period_buffer.resize(self.period_buffer.capacity(), 0); self.write_buf()?; - let pcm = self.pcm.as_mut().ok_or_else(|| { - io::Error::new(io::ErrorKind::Other, "Error stopping AlsaSink, PCM is None") - })?; + let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?; - pcm.drain().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("Error stopping AlsaSink {}", e), - ) - })?; + pcm.drain().map_err(AlsaError::DrainFailure)?; self.pcm = None; Ok(()) @@ -249,23 +263,28 @@ impl Sink for AlsaSink { } impl SinkAsBytes for AlsaSink { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { let mut start_index = 0; let data_len = data.len(); let capacity = self.period_buffer.capacity(); + loop { let data_left = data_len - start_index; let space_left = capacity - self.period_buffer.len(); let data_to_buffer = min(data_left, space_left); let end_index = start_index + data_to_buffer; + self.period_buffer .extend_from_slice(&data[start_index..end_index]); + if self.period_buffer.len() == capacity { self.write_buf()?; } + if end_index == data_len { break Ok(()); } + start_index = end_index; } } @@ -274,30 +293,18 @@ impl SinkAsBytes for AlsaSink { impl AlsaSink { pub const NAME: &'static str = "alsa"; - fn write_buf(&mut self) -> io::Result<()> { - let pcm = self.pcm.as_mut().ok_or_else(|| { - io::Error::new( - io::ErrorKind::Other, - "Error writing from AlsaSink buffer to PCM, PCM is None", - ) - })?; - let io = pcm.io_bytes(); - if let Err(err) = io.writei(&self.period_buffer) { + fn write_buf(&mut self) -> SinkResult<()> { + let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?; + + if let Err(e) = pcm.io_bytes().writei(&self.period_buffer) { // Capture and log the original error as a warning, and then try to recover. // If recovery fails then forward that error back to player. warn!( - "Error writing from AlsaSink buffer to PCM, trying to recover {}", - err + "Error writing from AlsaSink buffer to PCM, trying to recover, {}", + e ); - pcm.try_recover(err, false).map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!( - "Error writing from AlsaSink buffer to PCM, recovery failed {}", - e - ), - ) - })? + + pcm.try_recover(e, false).map_err(AlsaError::OnWrite)? } self.period_buffer.clear(); diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 58f6cbc9..8b957577 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,4 +1,4 @@ -use super::{Open, Sink, SinkAsBytes}; +use super::{Open, Sink, SinkAsBytes, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; @@ -11,7 +11,7 @@ use gst::prelude::*; use zerocopy::AsBytes; use std::sync::mpsc::{sync_channel, SyncSender}; -use std::{io, thread}; +use std::thread; #[allow(dead_code)] pub struct GstreamerSink { @@ -131,7 +131,7 @@ impl Sink for GstreamerSink { } impl SinkAsBytes for GstreamerSink { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { // Copy expensively (in to_vec()) to avoid thread synchronization self.tx .send(data.to_vec()) diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index a8f37524..5ba7b7ff 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,4 +1,4 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; @@ -6,7 +6,6 @@ use crate::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; -use std::io; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; pub struct JackSink { @@ -70,10 +69,11 @@ impl Open for JackSink { } impl Sink for JackSink { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { let samples = packet .samples() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + let samples_f32: &[f32] = &converter.f64_to_f32(samples); for sample in samples_f32.iter() { let res = self.send.send(*sample); diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 31fb847c..b89232b7 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,26 +1,40 @@ use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use std::io; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SinkError { + #[error("Audio Sink Error Not Connected: {0}")] + NotConnected(String), + #[error("Audio Sink Error Connection Refused: {0}")] + ConnectionRefused(String), + #[error("Audio Sink Error On Write: {0}")] + OnWrite(String), + #[error("Audio Sink Error Invalid Parameters: {0}")] + InvalidParams(String), +} + +pub type SinkResult = Result; pub trait Open { fn open(_: Option, format: AudioFormat) -> Self; } pub trait Sink { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { Ok(()) } - fn stop(&mut self) -> io::Result<()> { + fn stop(&mut self) -> SinkResult<()> { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()>; + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()>; } pub type SinkBuilder = fn(Option, AudioFormat) -> Box; pub trait SinkAsBytes { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()>; } fn mk_sink(device: Option, format: AudioFormat) -> Box { @@ -30,7 +44,7 @@ fn mk_sink(device: Option, format: AudioFormat // reuse code for various backends macro_rules! sink_as_bytes { () => { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { use crate::convert::i24; use zerocopy::AsBytes; match packet { diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 56040384..fd804a0e 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,4 +1,4 @@ -use super::{Open, Sink, SinkAsBytes}; +use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; @@ -23,14 +23,14 @@ impl Open for StdoutSink { } impl Sink for StdoutSink { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { if self.output.is_none() { let output: Box = match self.path.as_deref() { Some(path) => { let open_op = OpenOptions::new() .write(true) .open(path) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?; Box::new(open_op) } None => Box::new(io::stdout()), @@ -46,14 +46,18 @@ impl Sink for StdoutSink { } impl SinkAsBytes for StdoutSink { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { match self.output.as_deref_mut() { Some(output) => { - output.write_all(data)?; - output.flush()?; + output + .write_all(data) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + output + .flush() + .map_err(|e| SinkError::OnWrite(e.to_string()))?; } None => { - return Err(io::Error::new(io::ErrorKind::Other, "Output is None")); + return Err(SinkError::NotConnected("Output is None".to_string())); } } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 26355a03..7a0b179f 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,11 +1,10 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; -use std::io; use std::process::exit; use std::time::Duration; @@ -96,7 +95,7 @@ impl<'a> Open for PortAudioSink<'a> { } impl<'a> Sink for PortAudioSink<'a> { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { macro_rules! start_sink { (ref mut $stream: ident, ref $parameters: ident) => {{ if $stream.is_none() { @@ -125,7 +124,7 @@ impl<'a> Sink for PortAudioSink<'a> { Ok(()) } - fn stop(&mut self) -> io::Result<()> { + fn stop(&mut self) -> SinkResult<()> { macro_rules! stop_sink { (ref mut $stream: ident) => {{ $stream.as_mut().unwrap().stop().unwrap(); @@ -141,7 +140,7 @@ impl<'a> Sink for PortAudioSink<'a> { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { macro_rules! write_sink { (ref mut $stream: expr, $samples: expr) => { $stream.as_mut().unwrap().write($samples) @@ -150,7 +149,7 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet .samples() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| SinkError::OnWrite(e.to_string()))?; let result = match self { Self::F32(stream, _parameters) => { diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 4ef8317a..7487517f 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,11 +1,10 @@ -use super::{Open, Sink, SinkAsBytes}; +use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, error::PAErr, stream::Direction}; use libpulse_simple_binding::Simple; -use std::io; use thiserror::Error; const APP_NAME: &str = "librespot"; @@ -13,18 +12,40 @@ const STREAM_NAME: &str = "Spotify endpoint"; #[derive(Debug, Error)] enum PulseError { - #[error("Error starting PulseAudioSink, invalid PulseAudio sample spec")] - InvalidSampleSpec, - #[error("Error starting PulseAudioSink, could not connect to PulseAudio server, {0}")] + #[error(" Unsupported Pulseaudio Sample Spec, Format {pulse_format:?} ({format:?}), Channels {channels}, Rate {rate}")] + InvalidSampleSpec { + pulse_format: pulse::sample::Format, + format: AudioFormat, + channels: u8, + rate: u32, + }, + + #[error(" {0}")] ConnectionRefused(PAErr), - #[error("Error stopping PulseAudioSink, failed to drain PulseAudio server buffer, {0}")] + + #[error(" Failed to Drain Pulseaudio Buffer, {0}")] DrainFailure(PAErr), - #[error("Error in PulseAudioSink, Not connected to PulseAudio server")] + + #[error("")] NotConnected, - #[error("Error writing from PulseAudioSink to PulseAudio server, {0}")] + + #[error(" {0}")] OnWrite(PAErr), } +impl From for SinkError { + fn from(e: PulseError) -> SinkError { + use PulseError::*; + let es = e.to_string(); + match e { + DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es), + ConnectionRefused(_) => SinkError::ConnectionRefused(es), + NotConnected => SinkError::NotConnected(es), + InvalidSampleSpec { .. } => SinkError::InvalidParams(es), + } + } +} + pub struct PulseAudioSink { s: Option, device: Option, @@ -51,68 +72,57 @@ impl Open for PulseAudioSink { } impl Sink for PulseAudioSink { - fn start(&mut self) -> io::Result<()> { - if self.s.is_some() { - return Ok(()); - } + fn start(&mut self) -> SinkResult<()> { + if self.s.is_none() { + // PulseAudio calls S24 and S24_3 different from the rest of the world + let pulse_format = match self.format { + AudioFormat::F32 => pulse::sample::Format::FLOAT32NE, + AudioFormat::S32 => pulse::sample::Format::S32NE, + AudioFormat::S24 => pulse::sample::Format::S24_32NE, + AudioFormat::S24_3 => pulse::sample::Format::S24NE, + AudioFormat::S16 => pulse::sample::Format::S16NE, + _ => unreachable!(), + }; - // PulseAudio calls S24 and S24_3 different from the rest of the world - let pulse_format = match self.format { - AudioFormat::F32 => pulse::sample::Format::FLOAT32NE, - AudioFormat::S32 => pulse::sample::Format::S32NE, - AudioFormat::S24 => pulse::sample::Format::S24_32NE, - AudioFormat::S24_3 => pulse::sample::Format::S24NE, - AudioFormat::S16 => pulse::sample::Format::S16NE, - _ => unreachable!(), - }; + let ss = pulse::sample::Spec { + format: pulse_format, + channels: NUM_CHANNELS, + rate: SAMPLE_RATE, + }; - let ss = pulse::sample::Spec { - format: pulse_format, - channels: NUM_CHANNELS, - rate: SAMPLE_RATE, - }; + if !ss.is_valid() { + let pulse_error = PulseError::InvalidSampleSpec { + pulse_format, + format: self.format, + channels: NUM_CHANNELS, + rate: SAMPLE_RATE, + }; - if !ss.is_valid() { - return Err(io::Error::new( - io::ErrorKind::Other, - PulseError::InvalidSampleSpec, - )); - } - - let result = Simple::new( - None, // Use the default server. - APP_NAME, // Our application's name. - Direction::Playback, // Direction. - self.device.as_deref(), // Our device (sink) name. - STREAM_NAME, // Description of our stream. - &ss, // Our sample format. - None, // Use default channel map. - None, // Use default buffering attributes. - ); - - match result { - Ok(s) => { - self.s = Some(s); - } - Err(e) => { - return Err(io::Error::new( - io::ErrorKind::ConnectionRefused, - PulseError::ConnectionRefused(e), - )); + return Err(SinkError::from(pulse_error)); } + + let s = Simple::new( + None, // Use the default server. + APP_NAME, // Our application's name. + Direction::Playback, // Direction. + self.device.as_deref(), // Our device (sink) name. + STREAM_NAME, // Description of our stream. + &ss, // Our sample format. + None, // Use default channel map. + None, // Use default buffering attributes. + ) + .map_err(PulseError::ConnectionRefused)?; + + self.s = Some(s); } Ok(()) } - fn stop(&mut self) -> io::Result<()> { - let s = self - .s - .as_mut() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, PulseError::NotConnected))?; + fn stop(&mut self) -> SinkResult<()> { + let s = self.s.as_mut().ok_or(PulseError::NotConnected)?; - s.drain() - .map_err(|e| io::Error::new(io::ErrorKind::Other, PulseError::DrainFailure(e)))?; + s.drain().map_err(PulseError::DrainFailure)?; self.s = None; Ok(()) @@ -122,14 +132,10 @@ impl Sink for PulseAudioSink { } impl SinkAsBytes for PulseAudioSink { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { - let s = self - .s - .as_mut() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, PulseError::NotConnected))?; + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { + let s = self.s.as_mut().ok_or(PulseError::NotConnected)?; - s.write(data) - .map_err(|e| io::Error::new(io::ErrorKind::Other, PulseError::OnWrite(e)))?; + s.write(data).map_err(PulseError::OnWrite)?; Ok(()) } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 4d9c65c5..200c9fc4 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,11 +1,11 @@ use std::process::exit; +use std::thread; use std::time::Duration; -use std::{io, thread}; use cpal::traits::{DeviceTrait, HostTrait}; use thiserror::Error; -use super::Sink; +use super::{Sink, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; @@ -33,16 +33,30 @@ pub fn mk_rodiojack(device: Option, format: AudioFormat) -> Box No Device Available")] NoDeviceAvailable, - #[error("Rodio: device \"{0}\" is not available")] + #[error(" device \"{0}\" is Not Available")] DeviceNotAvailable(String), - #[error("Rodio play error: {0}")] + #[error(" Play Error: {0}")] PlayError(#[from] rodio::PlayError), - #[error("Rodio stream error: {0}")] + #[error(" Stream Error: {0}")] StreamError(#[from] rodio::StreamError), - #[error("Cannot get audio devices: {0}")] + #[error(" Cannot Get Audio Devices: {0}")] DevicesError(#[from] cpal::DevicesError), + #[error(" {0}")] + Samples(String), +} + +impl From for SinkError { + fn from(e: RodioError) -> SinkError { + use RodioError::*; + let es = e.to_string(); + match e { + StreamError(_) | PlayError(_) | Samples(_) => SinkError::OnWrite(es), + NoDeviceAvailable | DeviceNotAvailable(_) => SinkError::ConnectionRefused(es), + DevicesError(_) => SinkError::InvalidParams(es), + } + } } pub struct RodioSink { @@ -175,10 +189,10 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro } impl Sink for RodioSink { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { let samples = packet .samples() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| RodioError::Samples(e.to_string()))?; match self.format { AudioFormat::F32 => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 63a88c22..6272fa32 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,11 +1,11 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; +use std::thread; use std::time::Duration; -use std::{io, thread}; pub enum SdlSink { F32(AudioQueue), @@ -52,7 +52,7 @@ impl Open for SdlSink { } impl Sink for SdlSink { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { macro_rules! start_sink { ($queue: expr) => {{ $queue.clear(); @@ -67,7 +67,7 @@ impl Sink for SdlSink { Ok(()) } - fn stop(&mut self) -> io::Result<()> { + fn stop(&mut self) -> SinkResult<()> { macro_rules! stop_sink { ($queue: expr) => {{ $queue.pause(); @@ -82,7 +82,7 @@ impl Sink for SdlSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { macro_rules! drain_sink { ($queue: expr, $size: expr) => {{ // sleep and wait for sdl thread to drain the queue a bit @@ -94,7 +94,7 @@ impl Sink for SdlSink { let samples = packet .samples() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| SinkError::OnWrite(e.to_string()))?; match self { Self::F32(queue) => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 64f04c88..c501cf83 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,10 +1,10 @@ -use super::{Open, Sink, SinkAsBytes}; +use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; use shell_words::split; -use std::io::{self, Write}; +use std::io::Write; use std::process::{Child, Command, Stdio}; pub struct SubprocessSink { @@ -30,21 +30,25 @@ impl Open for SubprocessSink { } impl Sink for SubprocessSink { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { let args = split(&self.shell_command).unwrap(); - self.child = Some( - Command::new(&args[0]) - .args(&args[1..]) - .stdin(Stdio::piped()) - .spawn()?, - ); + let child = Command::new(&args[0]) + .args(&args[1..]) + .stdin(Stdio::piped()) + .spawn() + .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?; + self.child = Some(child); Ok(()) } - fn stop(&mut self) -> io::Result<()> { + fn stop(&mut self) -> SinkResult<()> { if let Some(child) = &mut self.child.take() { - child.kill()?; - child.wait()?; + child + .kill() + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + child + .wait() + .map_err(|e| SinkError::OnWrite(e.to_string()))?; } Ok(()) } @@ -53,11 +57,18 @@ impl Sink for SubprocessSink { } impl SinkAsBytes for SubprocessSink { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { if let Some(child) = &mut self.child { - let child_stdin = child.stdin.as_mut().unwrap(); - child_stdin.write_all(data)?; - child_stdin.flush()?; + let child_stdin = child + .stdin + .as_mut() + .ok_or_else(|| SinkError::NotConnected("Child is None".to_string()))?; + child_stdin + .write_all(data) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + child_stdin + .flush() + .map_err(|e| SinkError::OnWrite(e.to_string()))?; } Ok(()) } From 4c1b2278abe8c2a83d7f31e56435f735af208891 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 4 Oct 2021 13:59:18 -0500 Subject: [PATCH 23/95] Fix clippy comparison chain warning (#857) --- playback/src/audio_backend/alsa.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 17798868..41c75ed6 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -7,6 +7,7 @@ use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, HwParams, PCM}; use alsa::{Direction, ValueOr}; use std::cmp::min; +use std::cmp::Ordering; use std::process::exit; use std::time::Duration; use thiserror::Error; @@ -92,7 +93,7 @@ fn list_outputs() -> SinkResult<()> { for t in &["pcm", "ctl", "hwdep"] { println!("{} devices:", t); - let i = HintIter::new_str(None, &t).map_err(|_| AlsaError::Parsing)?; + let i = HintIter::new_str(None, t).map_err(|_| AlsaError::Parsing)?; for a in i { if let Some(Direction::Playback) = a.direction { @@ -225,14 +226,16 @@ impl Sink for AlsaSink { let (pcm, bytes_per_period) = open_device(&self.device, self.format)?; self.pcm = Some(pcm); - let current_capacity = self.period_buffer.capacity(); - - if current_capacity > bytes_per_period { - self.period_buffer.truncate(bytes_per_period); - self.period_buffer.shrink_to_fit(); - } else if current_capacity < bytes_per_period { - let extra = bytes_per_period - self.period_buffer.len(); - self.period_buffer.reserve_exact(extra); + match self.period_buffer.capacity().cmp(&bytes_per_period) { + Ordering::Greater => { + self.period_buffer.truncate(bytes_per_period); + self.period_buffer.shrink_to_fit(); + } + Ordering::Less => { + let extra = bytes_per_period - self.period_buffer.len(); + self.period_buffer.reserve_exact(extra); + } + Ordering::Equal => (), } // Should always match the "Period Buffer size in bytes: " trace! message. @@ -251,11 +254,10 @@ impl Sink for AlsaSink { self.period_buffer.resize(self.period_buffer.capacity(), 0); self.write_buf()?; - let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?; + let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?; pcm.drain().map_err(AlsaError::DrainFailure)?; - self.pcm = None; Ok(()) } From 095536f100b1ae428315f94e166373906fd54a91 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 4 Oct 2021 21:44:03 +0200 Subject: [PATCH 24/95] Prepare for 0.3.0 release --- CHANGELOG.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a056bc2..0fc3f9cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. ## [Unreleased] + +## [0.3.0] - YYYY-MM-DD + ### Added - [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`. - [playback] Add support for dithering with `--dither` for lower requantization error (breaking) - [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves - [playback] `alsamixer`: support for querying dB range from Alsa softvol - [playback] Add `--format F64` (supported by Alsa and GStreamer only) -- [playback] Add `--normalisation-type auto` that switches between album and track automatically +- [playback] Add `--normalisation-gain-type auto` that switches between album and track automatically ### Changed - [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `DecoderError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) @@ -23,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] Make cubic volume control available to all mixers with `--volume-ctrl cubic` - [playback] Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking) - [playback] `alsamixer`: complete rewrite (breaking) -- [playback] `alsamixer`: query card dB range for the `log` volume control unless specified otherwise +- [playback] `alsamixer`: query card dB range for the volume control unless specified otherwise - [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise - [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking) - [playback] `player`: make `convert` and `decoder` public so you can implement your own `Sink` @@ -67,7 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2019-11-06 -[unreleased]: https://github.com/librespot-org/librespot/compare/v0.2.0..HEAD +[unreleased]: https://github.com/librespot-org/librespot/compare/v0.3.0..HEAD +[0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0 [0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6..v0.2.0 [0.1.6]: https://github.com/librespot-org/librespot/compare/v0.1.5..v0.1.6 [0.1.5]: https://github.com/librespot-org/librespot/compare/v0.1.3..v0.1.5 From 289b4f9bcceeee1e046986ba3337f75e0eea320a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 5 Oct 2021 22:08:26 +0200 Subject: [PATCH 25/95] Fix behavior after last track of an album/playlist * When autoplay is disabled, then loop back to the first track instead of 10 tracks back. Continue or stop playing depending on the state of the repeat button. * When autoplay is enabled, then extend the playlist *after* the last track. #844 broke this such that the last track of an album or playlist was never played. Fixes: #434 --- CHANGELOG.md | 1 + connect/src/spirc.rs | 45 +++++++++++++++----------------------------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fc3f9cc..196b4e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - [connect] Fix step size on volume up/down events +- [connect] Fix looping back to the first track after the last track of an album or playlist - [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream - [playback] Fix `log` and `cubic` volume controls to be mute at zero volume - [playback] Fix `S24_3` format on big-endian systems diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 9aa86134..2038c8bd 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -84,7 +84,6 @@ struct SpircTaskConfig { autoplay: bool, } -const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; const VOLUME_STEPS: i64 = 64; @@ -887,8 +886,8 @@ impl SpircTask { let tracks_len = self.state.get_track().len() as u32; debug!( "At track {:?} of {:?} <{:?}> update [{}]", - new_index, - self.state.get_track().len(), + new_index + 1, + tracks_len, self.state.get_context_uri(), tracks_len - new_index < CONTEXT_FETCH_THRESHOLD ); @@ -902,27 +901,25 @@ impl SpircTask { self.context_fut = self.resolve_station(&context_uri); self.update_tracks_from_context(); } - let last_track = new_index == tracks_len - 1; - if self.config.autoplay && last_track { - // Extend the playlist - // Note: This doesn't seem to reflect in the UI - // the additional tracks in the frame don't show up as with station view - debug!("Extending playlist <{}>", context_uri); - self.update_tracks_from_context(); - } if new_index >= tracks_len { - new_index = 0; // Loop around back to start - continue_playing = self.state.get_repeat(); + if self.config.autoplay { + // Extend the playlist + debug!("Extending playlist <{}>", context_uri); + self.update_tracks_from_context(); + self.player.set_auto_normalise_as_album(false); + } else { + new_index = 0; + continue_playing = self.state.get_repeat(); + debug!( + "Looping around back to start, repeat is {}", + continue_playing + ); + } } if tracks_len > 0 { self.state.set_playing_track_index(new_index); self.load_track(continue_playing, 0); - if self.config.autoplay && last_track { - // If we're now playing the last track of an album, then - // switch to track normalisation mode for the autoplay to come. - self.player.set_auto_normalise_as_album(false); - } } else { info!("Not playing next track because there are no more tracks left in queue."); self.state.set_playing_track_index(0); @@ -1054,21 +1051,9 @@ impl SpircTask { let new_tracks = &context.tracks; debug!("Adding {:?} tracks from context to frame", new_tracks.len()); let mut track_vec = self.state.take_track().into_vec(); - if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { - track_vec.drain(0..head); - } track_vec.extend_from_slice(new_tracks); self.state .set_track(protobuf::RepeatedField::from_vec(track_vec)); - - // Update playing index - if let Some(new_index) = self - .state - .get_playing_track_index() - .checked_sub(CONTEXT_TRACKS_HISTORY as u32) - { - self.state.set_playing_track_index(new_index); - } } else { warn!("No context to update from!"); } From 0f5d610b4bc681ea9c956f5768ea1093620e9812 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 6 Oct 2021 21:21:03 +0200 Subject: [PATCH 26/95] Revert 10 track history window --- connect/src/spirc.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 2038c8bd..d644e2b0 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -84,6 +84,7 @@ struct SpircTaskConfig { autoplay: bool, } +const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; const VOLUME_STEPS: i64 = 64; @@ -1051,9 +1052,21 @@ impl SpircTask { let new_tracks = &context.tracks; debug!("Adding {:?} tracks from context to frame", new_tracks.len()); let mut track_vec = self.state.take_track().into_vec(); + if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { + track_vec.drain(0..head); + } track_vec.extend_from_slice(new_tracks); self.state .set_track(protobuf::RepeatedField::from_vec(track_vec)); + + // Update playing index + if let Some(new_index) = self + .state + .get_playing_track_index() + .checked_sub(CONTEXT_TRACKS_HISTORY as u32) + { + self.state.set_playing_track_index(new_index); + } } else { warn!("No context to update from!"); } From 9ef53f5ffb2cc3acc134854dffa2ef45e119a170 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 6 Oct 2021 11:20:09 -0500 Subject: [PATCH 27/95] simplify buffer resizing This way is less verbose, much more simple and less brittle. --- playback/src/audio_backend/alsa.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 41c75ed6..9dd3ea0c 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -7,7 +7,6 @@ use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, HwParams, PCM}; use alsa::{Direction, ValueOr}; use std::cmp::min; -use std::cmp::Ordering; use std::process::exit; use std::time::Duration; use thiserror::Error; @@ -226,16 +225,8 @@ impl Sink for AlsaSink { let (pcm, bytes_per_period) = open_device(&self.device, self.format)?; self.pcm = Some(pcm); - match self.period_buffer.capacity().cmp(&bytes_per_period) { - Ordering::Greater => { - self.period_buffer.truncate(bytes_per_period); - self.period_buffer.shrink_to_fit(); - } - Ordering::Less => { - let extra = bytes_per_period - self.period_buffer.len(); - self.period_buffer.reserve_exact(extra); - } - Ordering::Equal => (), + if self.period_buffer.capacity() != bytes_per_period { + self.period_buffer = Vec::with_capacity(bytes_per_period); } // Should always match the "Period Buffer size in bytes: " trace! message. From 6a3377402a5910841968adbc651ded08ae825ad9 Mon Sep 17 00:00:00 2001 From: Sasha Hilton Date: Wed, 13 Oct 2021 15:10:18 +0100 Subject: [PATCH 28/95] Update version numbers to 0.3.0 --- Cargo.toml | 16 ++++++++-------- audio/Cargo.toml | 4 ++-- connect/Cargo.toml | 10 +++++----- core/Cargo.toml | 4 ++-- discovery/Cargo.toml | 4 ++-- metadata/Cargo.toml | 6 +++--- playback/Cargo.toml | 8 ++++---- protocol/Cargo.toml | 2 +- publish.sh | 2 +- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ced7d0f9..90704c84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.2.0" +version = "0.3.0" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -22,31 +22,31 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-connect] path = "connect" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-core] path = "core" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-discovery] path = "discovery" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-metadata] path = "metadata" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-playback] path = "playback" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-protocol] path = "protocol" -version = "0.2.0" +version = "0.3.0" [dependencies] base64 = "0.13" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index f4440592..5c2a43be 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Lietar "] description="The audio fetching and processing logic for librespot" license="MIT" @@ -8,7 +8,7 @@ edition = "2018" [dependencies.librespot-core] path = "../core" -version = "0.2.0" +version = "0.3.0" [dependencies] aes-ctr = "0.6" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 89d185ab..dd0848d4 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" @@ -20,19 +20,19 @@ tokio-stream = "0.1.1" [dependencies.librespot-core] path = "../core" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-playback] path = "../playback" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-discovery] path = "../discovery" -version = "0.2.0" +version = "0.3.0" [features] with-dns-sd = ["librespot-discovery/with-dns-sd"] diff --git a/core/Cargo.toml b/core/Cargo.toml index 24e599a6..60c53a3e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" @@ -10,7 +10,7 @@ edition = "2018" [dependencies.librespot-protocol] path = "../protocol" -version = "0.2.0" +version = "0.3.0" [dependencies] aes = "0.6" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 9ea9df48..cdf1f342 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" @@ -28,7 +28,7 @@ dns-sd = { version = "0.1.3", optional = true } [dependencies.librespot-core] path = "../core" default_features = false -version = "0.2.0" +version = "0.3.0" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 6e181a1a..6ea90033 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" @@ -15,7 +15,7 @@ log = "0.4" [dependencies.librespot-core] path = "../core" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.2.0" +version = "0.3.0" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index f2fdaf48..dfab7168 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.2.0" +version = "0.3.0" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" @@ -9,13 +9,13 @@ edition = "2018" [dependencies.librespot-audio] path = "../audio" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-core] path = "../core" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-metadata] path = "../metadata" -version = "0.2.0" +version = "0.3.0" [dependencies] futures-executor = "0.3" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 5c3ae084..83c3a42b 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" diff --git a/publish.sh b/publish.sh index 478741a5..fb4a475a 100755 --- a/publish.sh +++ b/publish.sh @@ -6,7 +6,7 @@ DRY_RUN='false' WORKINGDIR="$( cd "$(dirname "$0")" ; pwd -P )" cd $WORKINGDIR -crates=( "protocol" "core" "audio" "metadata" "playback" "connect" "librespot" ) +crates=( "protocol" "core" "discovery" "audio" "metadata" "playback" "connect" "librespot" ) function switchBranch { if [ "$SKIP_MERGE" = 'false' ] ; then From afbdd11f4597375e1cc540e03033d0889b47f220 Mon Sep 17 00:00:00 2001 From: Sasha Hilton Date: Wed, 13 Oct 2021 15:30:13 +0100 Subject: [PATCH 29/95] Update Cargo.lock for 0.3.0 --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e94d21b6..76f4e53e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,7 +1131,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.2.0" +version = "0.3.0" dependencies = [ "base64", "env_logger", @@ -1156,7 +1156,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.2.0" +version = "0.3.0" dependencies = [ "aes-ctr", "byteorder", @@ -1170,7 +1170,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.2.0" +version = "0.3.0" dependencies = [ "form_urlencoded", "futures-util", @@ -1189,7 +1189,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "aes", "base64", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.2.0" +version = "0.3.0" dependencies = [ "aes-ctr", "base64", @@ -1254,7 +1254,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.2.0" +version = "0.3.0" dependencies = [ "async-trait", "byteorder", @@ -1266,7 +1266,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.2.0" +version = "0.3.0" dependencies = [ "alsa", "byteorder", @@ -1298,7 +1298,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.2.0" +version = "0.3.0" dependencies = [ "glob", "protobuf", From d99581aeb774da681910f5bca22b74d654fac83b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Oct 2021 20:37:46 +0200 Subject: [PATCH 30/95] Tag 0.3.0 and document #859 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 196b4e88..efd59d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.3.0] - YYYY-MM-DD +### Fixed +- [connect] Partly fix behavior after last track of an album/playlist + +## [0.3.0] - 2021-10-13 ### Added - [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`. From 3b51a5dc23c43b028d1e2ecd19b5fc65c14cf1ae Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Thu, 14 Oct 2021 11:57:33 +0100 Subject: [PATCH 31/95] Include build profile in the displayed version information Example output from -V for a debug build is: librespot 0.3.0 832889b (Built on 2021-10-14, Build ID: ANJrycbG, Profile: debug) --- CHANGELOG.md | 3 +++ src/main.rs | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efd59d05..9a383436 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Include build profile in the displayed version information + ### Fixed - [connect] Partly fix behavior after last track of an album/playlist diff --git a/src/main.rs b/src/main.rs index 76e8ba1c..01ec460b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -160,14 +160,20 @@ pub fn parse_file_size(input: &str) -> Result { Ok((num * base.pow(exponent) as f64) as u64) } -fn print_version() { - println!( - "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", +fn get_version_string() -> String { + #[cfg(debug_assertions)] + const BUILD_PROFILE: &str = "debug"; + #[cfg(not(debug_assertions))] + const BUILD_PROFILE: &str = "release"; + + format!( + "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id}, Profile: {build_profile})", semver = version::SEMVER, sha = version::SHA_SHORT, build_date = version::BUILD_DATE, - build_id = version::BUILD_ID - ); + build_id = version::BUILD_ID, + build_profile = BUILD_PROFILE + ) } struct Setup { @@ -438,20 +444,14 @@ fn get_setup(args: &[String]) -> Setup { } if matches.opt_present(VERSION) { - print_version(); + println!("{}", get_version_string()); exit(0); } let verbose = matches.opt_present(VERBOSE); setup_logging(verbose); - info!( - "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", - semver = version::SEMVER, - sha = version::SHA_SHORT, - build_date = version::BUILD_DATE, - build_id = version::BUILD_ID - ); + info!("{}", get_version_string()); let backend_name = matches.opt_str(BACKEND); if backend_name == Some("?".into()) { From 4c89a721eeed791bdd85120df80e0e83ca9caff3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 19 Oct 2021 22:33:04 +0200 Subject: [PATCH 32/95] Improve dithering CPU usage (#866) --- CHANGELOG.md | 1 + Cargo.lock | 14 ++++++++++++-- playback/Cargo.toml | 1 + playback/src/dither.rs | 36 ++++++++++++++++++++++++++---------- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a383436..fb79169b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Include build profile in the displayed version information +- [playback] Improve dithering CPU usage by about 33% ### Fixed - [connect] Partly fix behavior after last track of an album/playlist diff --git a/Cargo.lock b/Cargo.lock index 76f4e53e..31018365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,6 +1288,7 @@ dependencies = [ "portaudio-rs", "rand", "rand_distr", + "rand_xoshiro", "rodio", "sdl2", "shell-words", @@ -1881,9 +1882,9 @@ dependencies = [ [[package]] name = "rand_distr" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051b398806e42b9cd04ad9ec8f81e355d0a382c543ac6672c62f5a5b452ef142" +checksum = "964d548f8e7d12e102ef183a0de7e98180c9f8729f555897a857b96e48122d2f" dependencies = [ "num-traits", "rand", @@ -1898,6 +1899,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + [[package]] name = "redox_syscall" version = "0.2.10" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index dfab7168..b3b39559 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -49,6 +49,7 @@ ogg = "0.8" # Dithering rand = "0.8" rand_distr = "0.4" +rand_xoshiro = "0.6" [features] alsa-backend = ["alsa"] diff --git a/playback/src/dither.rs b/playback/src/dither.rs index 2510b886..a44acf21 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -1,4 +1,4 @@ -use rand::rngs::ThreadRng; +use rand::SeedableRng; use rand_distr::{Distribution, Normal, Triangular, Uniform}; use std::fmt; @@ -41,20 +41,36 @@ impl fmt::Display for dyn Ditherer { } } -// Implementation note: we save the handle to ThreadRng so it doesn't require -// a lookup on each call (which is on each sample!). This is ~2.5x as fast. -// Downside is that it is not Send so we cannot move it around player threads. +// `SmallRng` is 33% faster than `ThreadRng`, but we can do even better. +// `SmallRng` defaults to `Xoshiro256PlusPlus` on 64-bit platforms and +// `Xoshiro128PlusPlus` on 32-bit platforms. These are excellent for the +// general case. In our case of just 64-bit floating points, we can make +// some optimizations. Compared to `SmallRng`, these hand-picked generators +// improve performance by another 9% on 64-bit platforms and 2% on 32-bit +// platforms. // +// For reference, see https://prng.di.unimi.it. Note that we do not use +// `Xoroshiro128Plus` or `Xoshiro128Plus` because they display low linear +// complexity in the lower four bits, which is not what we want: +// linearization is the very point of dithering. +#[cfg(target_pointer_width = "64")] +type Rng = rand_xoshiro::Xoshiro256Plus; +#[cfg(not(target_pointer_width = "64"))] +type Rng = rand_xoshiro::Xoshiro128StarStar; + +fn create_rng() -> Rng { + Rng::from_entropy() +} pub struct TriangularDitherer { - cached_rng: ThreadRng, + cached_rng: Rng, distribution: Triangular, } impl Ditherer for TriangularDitherer { fn new() -> Self { Self { - cached_rng: rand::thread_rng(), + cached_rng: create_rng(), // 2 LSB peak-to-peak needed to linearize the response: distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(), } @@ -74,14 +90,14 @@ impl TriangularDitherer { } pub struct GaussianDitherer { - cached_rng: ThreadRng, + cached_rng: Rng, distribution: Normal, } impl Ditherer for GaussianDitherer { fn new() -> Self { Self { - cached_rng: rand::thread_rng(), + cached_rng: create_rng(), // 1/2 LSB RMS needed to linearize the response: distribution: Normal::new(0.0, 0.5).unwrap(), } @@ -103,7 +119,7 @@ impl GaussianDitherer { pub struct HighPassDitherer { active_channel: usize, previous_noises: [f64; NUM_CHANNELS], - cached_rng: ThreadRng, + cached_rng: Rng, distribution: Uniform, } @@ -112,7 +128,7 @@ impl Ditherer for HighPassDitherer { Self { active_channel: 0, previous_noises: [0.0; NUM_CHANNELS], - cached_rng: rand::thread_rng(), + cached_rng: create_rng(), distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB } } From ff3648434b5a8275a30d1bf1c86191a79b146c66 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 21 Oct 2021 19:31:58 +0200 Subject: [PATCH 33/95] Change hand-picked RNGs back to `SmallRng` While `Xoshiro256+` is faster on 64-bit, it has low linear complexity in the lower three bits, which *are* used when generating dither. Also, while `Xoshiro128StarStar` access one less variable from the heap, multiplication is generally slower than addition in hardware. --- Cargo.lock | 10 ---------- playback/Cargo.toml | 3 +-- playback/src/dither.rs | 28 ++++++---------------------- 3 files changed, 7 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31018365..82a9d460 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,7 +1288,6 @@ dependencies = [ "portaudio-rs", "rand", "rand_distr", - "rand_xoshiro", "rodio", "sdl2", "shell-words", @@ -1899,15 +1898,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core", -] - [[package]] name = "redox_syscall" version = "0.2.10" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index b3b39559..911800db 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -47,9 +47,8 @@ lewton = "0.10" ogg = "0.8" # Dithering -rand = "0.8" +rand = { version = "0.8", features = ["small_rng"] } rand_distr = "0.4" -rand_xoshiro = "0.6" [features] alsa-backend = ["alsa"] diff --git a/playback/src/dither.rs b/playback/src/dither.rs index a44acf21..0f667917 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -1,3 +1,4 @@ +use rand::rngs::SmallRng; use rand::SeedableRng; use rand_distr::{Distribution, Normal, Triangular, Uniform}; use std::fmt; @@ -41,29 +42,12 @@ impl fmt::Display for dyn Ditherer { } } -// `SmallRng` is 33% faster than `ThreadRng`, but we can do even better. -// `SmallRng` defaults to `Xoshiro256PlusPlus` on 64-bit platforms and -// `Xoshiro128PlusPlus` on 32-bit platforms. These are excellent for the -// general case. In our case of just 64-bit floating points, we can make -// some optimizations. Compared to `SmallRng`, these hand-picked generators -// improve performance by another 9% on 64-bit platforms and 2% on 32-bit -// platforms. -// -// For reference, see https://prng.di.unimi.it. Note that we do not use -// `Xoroshiro128Plus` or `Xoshiro128Plus` because they display low linear -// complexity in the lower four bits, which is not what we want: -// linearization is the very point of dithering. -#[cfg(target_pointer_width = "64")] -type Rng = rand_xoshiro::Xoshiro256Plus; -#[cfg(not(target_pointer_width = "64"))] -type Rng = rand_xoshiro::Xoshiro128StarStar; - -fn create_rng() -> Rng { - Rng::from_entropy() +fn create_rng() -> SmallRng { + SmallRng::from_entropy() } pub struct TriangularDitherer { - cached_rng: Rng, + cached_rng: SmallRng, distribution: Triangular, } @@ -90,7 +74,7 @@ impl TriangularDitherer { } pub struct GaussianDitherer { - cached_rng: Rng, + cached_rng: SmallRng, distribution: Normal, } @@ -119,7 +103,7 @@ impl GaussianDitherer { pub struct HighPassDitherer { active_channel: usize, previous_noises: [f64; NUM_CHANNELS], - cached_rng: Rng, + cached_rng: SmallRng, distribution: Uniform, } From a5c7580d4fe928b66e325753d9617f56ef2a7e5d Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 21 Oct 2021 17:24:02 -0500 Subject: [PATCH 34/95] Grammar Police the arg descriptions --- src/main.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main.rs b/src/main.rs index 01ec460b..a3522e8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -248,7 +248,7 @@ fn get_setup(args: &[String]) -> Setup { ).optopt( "", SYSTEM_CACHE, - "Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value.", + "Path to a directory where system files (credentials, volume) will be cached. May be different from the cache option value.", "PATH", ).optopt( "", @@ -257,7 +257,7 @@ fn get_setup(args: &[String]) -> Setup { "SIZE" ).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.") .optopt("n", NAME, "Device name.", "NAME") - .optopt("", DEVICE_TYPE, "Displayed device type.", "TYPE") + .optopt("", DEVICE_TYPE, "Displayed device type. Defaults to 'Speaker'.", "TYPE") .optopt( BITRATE, "bitrate", @@ -270,14 +270,14 @@ fn get_setup(args: &[String]) -> Setup { "Run PROGRAM when a playback event occurs.", "PROGRAM", ) - .optflag("", EMIT_SINK_EVENTS, "Run program set by --onevent before sink is opened and after it is closed.") + .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", VERSION, "Display librespot version string.") - .optopt("u", USERNAME, "Username to sign in with.", "USERNAME") - .optopt("p", PASSWORD, "Password", "PASSWORD") + .optopt("u", USERNAME, "Username used to sign in with.", "USERNAME") + .optopt("p", PASSWORD, "Password used to sign in with.", "PASSWORD") .optopt("", PROXY, "HTTP proxy to use when connecting.", "URL") - .optopt("", AP_PORT, "Connect to AP with specified port. If no AP with that port are present fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT") - .optflag("", DISABLE_DISCOVERY, "Disable discovery mode.") + .optopt("", AP_PORT, "Connect to an AP with a specified port. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT") + .optflag("", DISABLE_DISCOVERY, "Disable zeroconf discovery mode.") .optopt( "", BACKEND, @@ -287,7 +287,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", DEVICE, - "Audio device to use. Use '?' to list options if using alsa, portaudio or rodio.", + "Audio device to use. Use '?' to list options if using alsa, portaudio or rodio. Defaults to the backend's default.", "NAME", ) .optopt( @@ -299,10 +299,10 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", DITHER, - "Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", + "Specify the dither algorithm to use {none|gpdf|tpdf|tpdf_hp}. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", "DITHER", ) - .optopt("m", MIXER_TYPE, "Mixer to use {alsa|softvol}.", "MIXER") + .optopt("m", MIXER_TYPE, "Mixer to use {alsa|softvol}. Defaults to softvol", "MIXER") .optopt( "", "mixer-name", // deprecated @@ -312,7 +312,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", ALSA_MIXER_CONTROL, - "Alsa mixer control, e.g. 'PCM' or 'Master'. Defaults to 'PCM'.", + "Alsa mixer control, e.g. 'PCM', 'Master' or similar. Defaults to 'PCM'.", "NAME", ) .optopt( @@ -348,13 +348,13 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", ZEROCONF_PORT, - "The port the internal server advertised over zeroconf uses.", + "The port the internal server advertises over zeroconf.", "PORT", ) .optflag( "", ENABLE_VOLUME_NORMALISATION, - "Play all tracks at the same volume.", + "Play all tracks at approximately the same apparent volume.", ) .optopt( "", @@ -377,19 +377,19 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", NORMALISATION_THRESHOLD, - "Threshold (dBFS) to prevent clipping. Defaults to -2.0.", + "Threshold (dBFS) at which the dynamic limiter engages to prevent clipping. Defaults to -2.0.", "THRESHOLD", ) .optopt( "", NORMALISATION_ATTACK, - "Attack time (ms) in which the dynamic limiter is reducing gain. Defaults to 5.", + "Attack time (ms) in which the dynamic limiter reduces gain. Defaults to 5.", "TIME", ) .optopt( "", NORMALISATION_RELEASE, - "Release or decay time (ms) in which the dynamic limiter is restoring gain. Defaults to 100.", + "Release or decay time (ms) in which the dynamic limiter restores gain. Defaults to 100.", "TIME", ) .optopt( @@ -401,7 +401,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", VOLUME_CTRL, - "Volume control type {cubic|fixed|linear|log}. Defaults to log.", + "Volume control scale type {cubic|fixed|linear|log}. Defaults to log.", "VOLUME_CTRL" ) .optopt( @@ -423,7 +423,7 @@ fn get_setup(args: &[String]) -> Setup { .optflag( "", PASSTHROUGH, - "Pass raw stream to output, only works for pipe and subprocess.", + "Pass a raw stream to the output. Only works with the pipe and subprocess backends.", ); let matches = match opts.parse(&args[1..]) { From 9d19841c0f0b40208d3c0540c4d88cf98fc6356e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Oct 2021 20:07:11 +0200 Subject: [PATCH 35/95] Prepare for 0.3.1 release --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb79169b..6e362ae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.1] - 2021-10-24 + ### Changed - Include build profile in the displayed version information - [playback] Improve dithering CPU usage by about 33% @@ -78,7 +80,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2019-11-06 -[unreleased]: https://github.com/librespot-org/librespot/compare/v0.3.0..HEAD +[unreleased]: https://github.com/librespot-org/librespot/compare/v0.3.1..HEAD +[0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0..v0.3.1 [0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0 [0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6..v0.2.0 [0.1.6]: https://github.com/librespot-org/librespot/compare/v0.1.5..v0.1.6 From 0e6b1ba9dc426d1eec19f3ee01f5c899cb731654 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Oct 2021 20:12:33 +0200 Subject: [PATCH 36/95] Update version numbers to 0.3.1 --- Cargo.toml | 16 ++++++++-------- audio/Cargo.toml | 4 ++-- connect/Cargo.toml | 10 +++++----- core/Cargo.toml | 4 ++-- discovery/Cargo.toml | 4 ++-- metadata/Cargo.toml | 6 +++--- playback/Cargo.toml | 8 ++++---- protocol/Cargo.toml | 2 +- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 90704c84..8429ba2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.3.0" +version = "0.3.1" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -22,31 +22,31 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-connect] path = "connect" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-core] path = "core" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-discovery] path = "discovery" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-metadata] path = "metadata" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-playback] path = "playback" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-protocol] path = "protocol" -version = "0.3.0" +version = "0.3.1" [dependencies] base64 = "0.13" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 5c2a43be..77855e62 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Lietar "] description="The audio fetching and processing logic for librespot" license="MIT" @@ -8,7 +8,7 @@ edition = "2018" [dependencies.librespot-core] path = "../core" -version = "0.3.0" +version = "0.3.1" [dependencies] aes-ctr = "0.6" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index dd0848d4..4daf89f4 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" @@ -20,19 +20,19 @@ tokio-stream = "0.1.1" [dependencies.librespot-core] path = "../core" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-playback] path = "../playback" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-protocol] path = "../protocol" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-discovery] path = "../discovery" -version = "0.3.0" +version = "0.3.1" [features] with-dns-sd = ["librespot-discovery/with-dns-sd"] diff --git a/core/Cargo.toml b/core/Cargo.toml index 60c53a3e..2494a19a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" @@ -10,7 +10,7 @@ edition = "2018" [dependencies.librespot-protocol] path = "../protocol" -version = "0.3.0" +version = "0.3.1" [dependencies] aes = "0.6" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index cdf1f342..9b4d415e 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" @@ -28,7 +28,7 @@ dns-sd = { version = "0.1.3", optional = true } [dependencies.librespot-core] path = "../core" default_features = false -version = "0.3.0" +version = "0.3.1" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 6ea90033..8eb7be8c 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" @@ -15,7 +15,7 @@ log = "0.4" [dependencies.librespot-core] path = "../core" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-protocol] path = "../protocol" -version = "0.3.0" +version = "0.3.1" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 911800db..4e8d19c6 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.3.0" +version = "0.3.1" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" @@ -9,13 +9,13 @@ edition = "2018" [dependencies.librespot-audio] path = "../audio" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-core] path = "../core" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-metadata] path = "../metadata" -version = "0.3.0" +version = "0.3.1" [dependencies] futures-executor = "0.3" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 83c3a42b..38f76371 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" From c1ac4cbb3ad3bbdaeb6f8582186442c69cdae744 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Oct 2021 20:23:47 +0200 Subject: [PATCH 37/95] Update Cargo.lock for 0.3.1 --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82a9d460..1651f794 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,7 +1131,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.3.0" +version = "0.3.1" dependencies = [ "base64", "env_logger", @@ -1156,7 +1156,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.3.0" +version = "0.3.1" dependencies = [ "aes-ctr", "byteorder", @@ -1170,7 +1170,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.3.0" +version = "0.3.1" dependencies = [ "form_urlencoded", "futures-util", @@ -1189,7 +1189,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.3.0" +version = "0.3.1" dependencies = [ "aes", "base64", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.3.0" +version = "0.3.1" dependencies = [ "aes-ctr", "base64", @@ -1254,7 +1254,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.3.0" +version = "0.3.1" dependencies = [ "async-trait", "byteorder", @@ -1266,7 +1266,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.3.0" +version = "0.3.1" dependencies = [ "alsa", "byteorder", @@ -1298,7 +1298,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.3.0" +version = "0.3.1" dependencies = [ "glob", "protobuf", From 72b2c01b3ab8cf0c48cd89d450367148ff8b00c4 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 26 Oct 2021 20:10:39 +0200 Subject: [PATCH 38/95] Update crates --- Cargo.lock | 312 ++++++++++++++++++++++++++--------------------------- 1 file changed, 151 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1651f794..07f1e23d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" [[package]] name = "async-trait" @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" @@ -152,9 +152,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] name = "byteorder" @@ -164,15 +164,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.69" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" dependencies = [ "jobserver", ] @@ -228,13 +228,13 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "853eda514c284c2287f4bf20ae614f8781f40a81d32ecda6e91449304dfe077c" +checksum = "10612c0ec0e0a1ff0e97980647cb058a6e7aedb913d01d009c406b8b7d0b26ee" dependencies = [ "glob", "libc", - "libloading 0.7.0", + "libloading 0.7.1", ] [[package]] @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d47c1b11006b87e492b53b313bb699ce60e16613c4dddaa91f8f7c220ab2fa" +checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001" dependencies = [ "bytes", "memchr", @@ -260,9 +260,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "coreaudio-rs" @@ -311,9 +311,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" dependencies = [ "libc", ] @@ -439,9 +439,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" +checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" dependencies = [ "futures-channel", "futures-core", @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" dependencies = [ "futures-core", "futures-sink", @@ -464,15 +464,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" [[package]] name = "futures-executor" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" dependencies = [ "futures-core", "futures-task", @@ -481,15 +481,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" [[package]] name = "futures-macro" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" +checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" dependencies = [ "autocfg", "proc-macro-hack", @@ -500,21 +500,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" +checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" [[package]] name = "futures-task" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" [[package]] name = "futures-util" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" dependencies = [ "autocfg", "futures-channel", @@ -729,18 +729,18 @@ checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "headers" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" +checksum = "a4c4eb0471fcb85846d8b0690695ef354f9afb11cb03cac2e1d7c9253351afb0" dependencies = [ "base64", "bitflags", "bytes", "headers-core", "http", + "httpdate", "mime", "sha-1", - "time", ] [[package]] @@ -799,9 +799,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes", "fnv", @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ "bytes", "http", @@ -839,9 +839,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.11" +version = "0.14.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" +checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b" dependencies = [ "bytes", "futures-channel", @@ -894,9 +894,9 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28538916eb3f3976311f5dfbe67b5362d0add1293d0a9cad17debf86f8e3aa48" +checksum = "c9a83ec4af652890ac713ffd8dc859e650420a5ef47f7b9be29b6664ab50fbc8" dependencies = [ "if-addrs-sys", "libc", @@ -925,9 +925,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", ] @@ -943,15 +943,15 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "jack" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e720259b4a3e1f33cba335ca524a99a5f2411d405b05f6405fadd69269e2db" +checksum = "39722b9795ae57c6967da99b1ab009fe72897fcbc59be59508c7c520327d9e34" dependencies = [ "bitflags", "jack-sys", @@ -1001,9 +1001,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" dependencies = [ "wasm-bindgen", ] @@ -1033,9 +1033,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.99" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" +checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" [[package]] name = "libloading" @@ -1049,9 +1049,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" dependencies = [ "cfg-if 1.0.0", "winapi", @@ -1065,9 +1065,9 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" [[package]] name = "libmdns" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98477a6781ae1d6a1c2aeabfd2e23353a75fe8eb7c2545f6ed282ac8f3e2fc53" +checksum = "fac185a4d02e873c6d1ead59d674651f8ae5ec23ffe1637bee8de80665562a6a" dependencies = [ "byteorder", "futures-util", @@ -1083,9 +1083,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.24.0" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04b4154b9bc606019cb15125f96e08e1e9c4f53d55315f1ef69ae229e30d1765" +checksum = "86835d7763ded6bc16b6c0061ec60214da7550dfcd4ef93745f6f0096129676a" dependencies = [ "bitflags", "libc", @@ -1097,9 +1097,9 @@ dependencies = [ [[package]] name = "libpulse-simple-binding" -version = "2.24.0" +version = "2.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165af13c42b9c325582b1a75eaa4a0f176c9094bb3a13877826e9be24881231" +checksum = "d6a22538257c4d522bea6089d6478507f5d2589ea32150e20740aaaaaba44590" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -1108,9 +1108,9 @@ dependencies = [ [[package]] name = "libpulse-simple-sys" -version = "1.19.0" +version = "1.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83346d68605e656afdefa9a8a2f1968fa05ab9369b55f2e26f7bf2a11b7e8444" +checksum = "0b8b0fcb9665401cc7c156c337c8edc7eb4e797b9d3ae1667e1e9e17b29e0c7c" dependencies = [ "libpulse-sys", "pkg-config", @@ -1118,9 +1118,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.19.1" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ebed2cc92c38cac12307892ce6fb17e2e950bfda1ed17b3e1d47fd5184c8f2b" +checksum = "f12950b69c1b66233a900414befde36c8d4ea49deec1e1f34e4cd2f586e00c7d" dependencies = [ "libc", "num-derive", @@ -1307,9 +1307,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -1350,15 +1350,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" -[[package]] -name = "memoffset" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.16" @@ -1367,9 +1358,9 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", @@ -1476,15 +1467,14 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" [[package]] name = "nix" -version = "0.20.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8e5e343312e7fbeb2a52139114e9e702991ef9c2aea6817ff2440b35647d56" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" dependencies = [ "bitflags", "cc", "cfg-if 1.0.0", "libc", - "memoffset", ] [[package]] @@ -1586,7 +1576,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" dependencies = [ - "proc-macro-crate 1.0.0", + "proc-macro-crate 1.1.0", "proc-macro2", "quote", "syn", @@ -1638,9 +1628,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", @@ -1649,9 +1639,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if 1.0.0", "instant", @@ -1703,9 +1693,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" [[package]] name = "portaudio-rs" @@ -1730,9 +1720,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "pretty-hex" @@ -1742,9 +1732,9 @@ checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" [[package]] name = "priority-queue" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1340009a04e81f656a4e45e295f0b1191c81de424bf940c865e33577a8e223" +checksum = "cf40e51ccefb72d42720609e1d3c518de8b5800d723a09358d4a6d6245e1f8ca" dependencies = [ "autocfg", "indexmap", @@ -1761,9 +1751,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" dependencies = [ "thiserror", "toml", @@ -1807,33 +1797,33 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.28" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +checksum = "b581350bde2d774a19c6f30346796806b8f42b5fd3458c5f9a8623337fb27897" dependencies = [ "unicode-xid", ] [[package]] name = "protobuf" -version = "2.25.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020f86b07722c5c4291f7c723eac4676b3892d47d9a7708dc2779696407f039b" +checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754" [[package]] name = "protobuf-codegen" -version = "2.25.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b8ac7c5128619b0df145d9bace18e8ed057f18aebda1aa837a5525d4422f68c" +checksum = "3df8c98c08bd4d6653c2dbae00bd68c1d1d82a360265a5b0bbc73d48c63cb853" dependencies = [ "protobuf", ] [[package]] name = "protobuf-codegen-pure" -version = "2.25.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d0daa1b61d6e7a128cdca8c8604b3c5ee22c424c15c8d3a92fafffeda18aaf" +checksum = "394a73e2a819405364df8d30042c0f1174737a763e0170497ec9d36f8a2ea8f7" dependencies = [ "protobuf", "protobuf-codegen", @@ -1841,9 +1831,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -2019,18 +2009,18 @@ checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.127" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.127" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -2039,9 +2029,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", @@ -2050,9 +2040,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer", "cfg-if 1.0.0", @@ -2106,21 +2096,21 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "socket2" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" dependencies = [ "libc", "winapi", @@ -2164,9 +2154,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.74" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" dependencies = [ "proc-macro2", "quote", @@ -2175,9 +2165,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", @@ -2225,18 +2215,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -2255,9 +2245,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" dependencies = [ "tinyvec_macros", ] @@ -2270,9 +2260,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" +checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" dependencies = [ "autocfg", "bytes", @@ -2289,9 +2279,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd" dependencies = [ "proc-macro2", "quote", @@ -2311,9 +2301,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" dependencies = [ "bytes", "futures-core", @@ -2340,9 +2330,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.26" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -2351,9 +2341,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" dependencies = [ "lazy_static", ] @@ -2366,15 +2356,15 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" [[package]] name = "unicode-bidi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-normalization" @@ -2393,9 +2383,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-width" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" @@ -2476,9 +2466,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -2486,9 +2476,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" dependencies = [ "bumpalo", "lazy_static", @@ -2501,9 +2491,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2511,9 +2501,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ "proc-macro2", "quote", @@ -2524,15 +2514,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" [[package]] name = "web-sys" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" dependencies = [ "js-sys", "wasm-bindgen", From 52bd212e4357a755fd8b680be47f1ab68c822945 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 26 Oct 2021 22:06:52 -0500 Subject: [PATCH 39/95] Add disable credential cache flag As mentioned in https://github.com/librespot-org/librespot/discussions/870, this allows someone who would otherwise like to take advantage of audio file and volume caching to disable credential caching. --- core/src/cache.rs | 29 +++++++++++++++++++---------- src/main.rs | 17 +++++++++++++---- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/core/src/cache.rs b/core/src/cache.rs index 612b7c39..20270e3e 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -238,29 +238,38 @@ pub struct RemoveFileError(()); impl Cache { pub fn new>( - system_location: Option

, - audio_location: Option

, + credentials: Option

, + volume: Option

, + audio: Option

, size_limit: Option, ) -> io::Result { - if let Some(location) = &system_location { + let mut size_limiter = None; + + if let Some(location) = &credentials { fs::create_dir_all(location)?; } - let mut size_limiter = None; + let credentials_location = credentials + .as_ref() + .map(|p| p.as_ref().join("credentials.json")); - if let Some(location) = &audio_location { + if let Some(location) = &volume { fs::create_dir_all(location)?; + } + + let volume_location = volume.as_ref().map(|p| p.as_ref().join("volume")); + + if let Some(location) = &audio { + fs::create_dir_all(location)?; + if let Some(limit) = size_limit { let limiter = FsSizeLimiter::new(location.as_ref(), limit); + size_limiter = Some(Arc::new(limiter)); } } - let audio_location = audio_location.map(|p| p.as_ref().to_owned()); - let volume_location = system_location.as_ref().map(|p| p.as_ref().join("volume")); - let credentials_location = system_location - .as_ref() - .map(|p| p.as_ref().join("credentials.json")); + let audio_location = audio.map(|p| p.as_ref().to_owned()); let cache = Cache { credentials_location, diff --git a/src/main.rs b/src/main.rs index a3522e8c..c60b2887 100644 --- a/src/main.rs +++ b/src/main.rs @@ -203,6 +203,7 @@ fn get_setup(args: &[String]) -> Setup { const DEVICE: &str = "device"; const DEVICE_TYPE: &str = "device-type"; const DISABLE_AUDIO_CACHE: &str = "disable-audio-cache"; + const DISABLE_CREDENTIAL_CACHE: &str = "disable-credential-cache"; const DISABLE_DISCOVERY: &str = "disable-discovery"; const DISABLE_GAPLESS: &str = "disable-gapless"; const DITHER: &str = "dither"; @@ -256,6 +257,7 @@ fn get_setup(args: &[String]) -> Setup { "Limits the size of the cache for audio files.", "SIZE" ).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.") + .optflag("", DISABLE_CREDENTIAL_CACHE, "Disable caching of credentials.") .optopt("n", NAME, "Device name.", "NAME") .optopt("", DEVICE_TYPE, "Displayed device type. Defaults to 'Speaker'.", "TYPE") .optopt( @@ -560,10 +562,11 @@ fn get_setup(args: &[String]) -> Setup { let cache = { let audio_dir; - let system_dir; + let cred_dir; + let volume_dir; if matches.opt_present(DISABLE_AUDIO_CACHE) { audio_dir = None; - system_dir = matches + volume_dir = matches .opt_str(SYSTEM_CACHE) .or_else(|| matches.opt_str(CACHE)) .map(|p| p.into()); @@ -572,12 +575,18 @@ fn get_setup(args: &[String]) -> Setup { audio_dir = cache_dir .as_ref() .map(|p| AsRef::::as_ref(p).join("files")); - system_dir = matches + volume_dir = matches .opt_str(SYSTEM_CACHE) .or(cache_dir) .map(|p| p.into()); } + if matches.opt_present(DISABLE_CREDENTIAL_CACHE) { + cred_dir = None; + } else { + cred_dir = volume_dir.clone(); + } + let limit = if audio_dir.is_some() { matches .opt_str(CACHE_SIZE_LIMIT) @@ -593,7 +602,7 @@ fn get_setup(args: &[String]) -> Setup { None }; - match Cache::new(system_dir, audio_dir, limit) { + match Cache::new(cred_dir, volume_dir, audio_dir, limit) { Ok(cache) => Some(cache), Err(e) => { warn!("Cannot create cache: {}", e); From 9152ca81593d5083fa4da5f787f19696aab516fc Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 26 Oct 2021 22:18:10 -0500 Subject: [PATCH 40/95] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e362ae6..678880eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- [cache] Add `disable-credential-cache` flag (breaking). + ## [0.3.1] - 2021-10-24 ### Changed From 81e7c61c1789954a4604eb742cf1a7257a7bef3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=ABlle?= <27908024+jannuary@users.noreply.github.com> Date: Wed, 27 Oct 2021 20:03:14 +0700 Subject: [PATCH 41/95] README: Mention Spot --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 20afc01b..5dbb5487 100644 --- a/README.md +++ b/README.md @@ -116,3 +116,4 @@ functionality. - [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot. - [ncspot](https://github.com/hrkfdn/ncspot) - Cross-platform ncurses Spotify client. - [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot. +- [Spot](https://github.com/xou816/spot) - Gtk/Rust native Spotify client for the GNOME desktop. From e543ef72ede07b26f6bbb49b9109db8f3bec6c6b Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 27 Oct 2021 10:14:40 -0500 Subject: [PATCH 42/95] Clean up cache logic in main --- src/main.rs | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/main.rs b/src/main.rs index c60b2887..ae3258a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -561,31 +561,25 @@ fn get_setup(args: &[String]) -> Setup { }; let cache = { - let audio_dir; - let cred_dir; - let volume_dir; - if matches.opt_present(DISABLE_AUDIO_CACHE) { - audio_dir = None; - volume_dir = matches - .opt_str(SYSTEM_CACHE) - .or_else(|| matches.opt_str(CACHE)) - .map(|p| p.into()); - } else { - let cache_dir = matches.opt_str(CACHE); - audio_dir = cache_dir - .as_ref() - .map(|p| AsRef::::as_ref(p).join("files")); - volume_dir = matches - .opt_str(SYSTEM_CACHE) - .or(cache_dir) - .map(|p| p.into()); - } + let volume_dir = matches + .opt_str(SYSTEM_CACHE) + .or_else(|| matches.opt_str(CACHE)) + .map(|p| p.into()); - if matches.opt_present(DISABLE_CREDENTIAL_CACHE) { - cred_dir = None; + let cred_dir = if matches.opt_present(DISABLE_CREDENTIAL_CACHE) { + None } else { - cred_dir = volume_dir.clone(); - } + volume_dir.clone() + }; + + let audio_dir = if matches.opt_present(DISABLE_AUDIO_CACHE) { + None + } else { + matches + .opt_str(CACHE) + .as_ref() + .map(|p| AsRef::::as_ref(p).join("files")) + }; let limit = if audio_dir.is_some() { matches From 9e017119bb2aa24793c23c056c8c531fe7a8bceb Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 27 Oct 2021 14:47:33 -0500 Subject: [PATCH 43/95] Address review change request --- core/src/cache.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/core/src/cache.rs b/core/src/cache.rs index 20270e3e..da2ad022 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -238,28 +238,28 @@ pub struct RemoveFileError(()); impl Cache { pub fn new>( - credentials: Option

, - volume: Option

, - audio: Option

, + credentials_path: Option

, + volume_path: Option

, + audio_path: Option

, size_limit: Option, ) -> io::Result { let mut size_limiter = None; - if let Some(location) = &credentials { + if let Some(location) = &credentials_path { fs::create_dir_all(location)?; } - let credentials_location = credentials + let credentials_location = credentials_path .as_ref() .map(|p| p.as_ref().join("credentials.json")); - if let Some(location) = &volume { + if let Some(location) = &volume_path { fs::create_dir_all(location)?; } - let volume_location = volume.as_ref().map(|p| p.as_ref().join("volume")); + let volume_location = volume_path.as_ref().map(|p| p.as_ref().join("volume")); - if let Some(location) = &audio { + if let Some(location) = &audio_path { fs::create_dir_all(location)?; if let Some(limit) = size_limit { @@ -269,7 +269,7 @@ impl Cache { } } - let audio_location = audio.map(|p| p.as_ref().to_owned()); + let audio_location = audio_path.map(|p| p.as_ref().to_owned()); let cache = Cache { credentials_location, From 24e4d2b636e40c4adc039ca200d2f4611b502619 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 28 Oct 2021 09:10:10 -0500 Subject: [PATCH 44/95] Prevent librespot from becoming a zombie Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. --- CHANGELOG.md | 3 +++ src/main.rs | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 678880eb..a8da8d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [cache] Add `disable-credential-cache` flag (breaking). +### Fixed +- [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. + ## [0.3.1] - 2021-10-24 ### Changed diff --git a/src/main.rs b/src/main.rs index ae3258a1..51519013 100644 --- a/src/main.rs +++ b/src/main.rs @@ -647,6 +647,11 @@ fn get_setup(args: &[String]) -> Setup { ) }; + if credentials.is_none() && matches.opt_present(DISABLE_DISCOVERY) { + error!("Credentials are required if discovery is disabled."); + exit(1); + } + let session_config = { let device_id = device_id(&name); @@ -923,7 +928,8 @@ async fn main() { player_event_channel = Some(event_channel); }, Err(e) => { - warn!("Connection failed: {}", e); + error!("Connection failed: {}", e); + exit(1); } }, _ = async { spirc_task.as_mut().unwrap().await }, if spirc_task.is_some() => { From 0e9fdbe6b443c9d55dd9b9148ffc62a8e6d20c5b Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 30 Oct 2021 14:22:24 -0500 Subject: [PATCH 45/95] Refactor main.rs * Don't panic when parsing options. Instead list valid values and exit. * Get rid of needless .expect in playback/src/audio_backend/mod.rs. * Enforce reasonable ranges for option values (breaking). * Don't evaluate options that would otherwise have no effect. * Add pub const MIXERS to mixer/mod.rs very similar to the audio_backend's implementation. (non-breaking though) * Use different option descriptions and error messages based on what backends are enabled at build time. * Add a -q, --quiet option that changed the logging level to warn. * Add a short name for every flag and option. * Note removed options. * Other misc cleanups. --- CHANGELOG.md | 13 + core/src/config.rs | 12 + playback/src/audio_backend/mod.rs | 7 +- playback/src/config.rs | 4 +- playback/src/mixer/mod.rs | 18 +- src/main.rs | 1247 +++++++++++++++++++++-------- 6 files changed, 969 insertions(+), 332 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8da8d80..c480e03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- [main] Enforce reasonable ranges for option values (breaking). +- [main] Don't evaluate options that would otherwise have no effect. + ### Added - [cache] Add `disable-credential-cache` flag (breaking). +- [main] Use different option descriptions and error messages based on what backends are enabled at build time. +- [main] Add a `-q`, `--quiet` option that changes the logging level to warn. +- [main] Add a short name for every flag and option. ### Fixed - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. +- [main] Don't panic when parsing options. Instead list valid values and exit. + +### Removed +- [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. +- [playback] `alsamixer`: previously deprecated option `mixer-name` has been removed. +- [playback] `alsamixer`: previously deprecated option `mixer-index` has been removed. ## [0.3.1] - 2021-10-24 diff --git a/core/src/config.rs b/core/src/config.rs index 0e3eaf4a..b8c448c2 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -125,3 +125,15 @@ pub struct ConnectConfig { pub has_volume_ctrl: bool, pub autoplay: bool, } + +impl Default for ConnectConfig { + fn default() -> ConnectConfig { + ConnectConfig { + name: "Librespot".to_string(), + device_type: DeviceType::default(), + initial_volume: Some(50), + has_volume_ctrl: true, + autoplay: false, + } + } +} diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index b89232b7..4d3b0171 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -146,11 +146,6 @@ pub fn find(name: Option) -> Option { .find(|backend| name == backend.0) .map(|backend| backend.1) } else { - Some( - BACKENDS - .first() - .expect("No backends were enabled at build time") - .1, - ) + BACKENDS.first().map(|backend| backend.1) } } diff --git a/playback/src/config.rs b/playback/src/config.rs index c442faee..b8313bf4 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -76,7 +76,7 @@ impl AudioFormat { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum NormalisationType { Album, Track, @@ -101,7 +101,7 @@ impl Default for NormalisationType { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum NormalisationMethod { Basic, Dynamic, diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 5397598f..a3c7a5a1 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -53,11 +53,19 @@ fn mk_sink(config: MixerConfig) -> Box { Box::new(M::open(config)) } +pub const MIXERS: &[(&str, MixerFn)] = &[ + (SoftMixer::NAME, mk_sink::), // default goes first + #[cfg(feature = "alsa-backend")] + (AlsaMixer::NAME, mk_sink::), +]; + pub fn find(name: Option<&str>) -> Option { - match name { - None | Some(SoftMixer::NAME) => Some(mk_sink::), - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => Some(mk_sink::), - _ => None, + if let Some(name) = name { + MIXERS + .iter() + .find(|mixer| name == mixer.0) + .map(|mixer| mixer.1) + } else { + MIXERS.first().map(|mixer| mixer.1) } } diff --git a/src/main.rs b/src/main.rs index 51519013..990de629 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,15 +19,15 @@ use librespot::playback::config::{ use librespot::playback::dither; #[cfg(feature = "alsa-backend")] use librespot::playback::mixer::alsamixer::AlsaMixer; -use librespot::playback::mixer::mappings::MappedCtrl; use librespot::playback::mixer::{self, MixerConfig, MixerFn}; -use librespot::playback::player::{db_to_ratio, Player}; +use librespot::playback::player::{db_to_ratio, ratio_to_db, Player}; mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; use std::env; use std::io::{stderr, Write}; +use std::ops::RangeInclusive; use std::path::Path; use std::pin::Pin; use std::process::exit; @@ -44,7 +44,7 @@ fn usage(program: &str, opts: &getopts::Options) -> String { opts.usage(&brief) } -fn setup_logging(verbose: bool) { +fn setup_logging(quiet: bool, verbose: bool) { let mut builder = env_logger::Builder::new(); match env::var("RUST_LOG") { Ok(config) => { @@ -53,21 +53,29 @@ fn setup_logging(verbose: bool) { if verbose { warn!("`--verbose` flag overidden by `RUST_LOG` environment variable"); + } else if quiet { + warn!("`--quiet` flag overidden by `RUST_LOG` environment variable"); } } Err(_) => { if verbose { builder.parse_filters("libmdns=info,librespot=trace"); + } else if quiet { + builder.parse_filters("libmdns=warn,librespot=warn"); } else { builder.parse_filters("libmdns=info,librespot=info"); } builder.init(); + + if verbose && quiet { + warn!("`--verbose` and `--quiet` are mutually exclusive. Logging can not be both verbose and quiet. Using verbose mode."); + } } } } fn list_backends() { - println!("Available backends : "); + println!("Available backends: "); for (&(name, _), idx) in BACKENDS.iter().zip(0..) { if idx == 0 { println!("- {} (default)", name); @@ -194,11 +202,18 @@ struct Setup { } fn get_setup(args: &[String]) -> Setup { + const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; + const VALID_VOLUME_RANGE: RangeInclusive = 0.0..=100.0; + const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; + const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive = -10.0..=0.0; + const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive = 1..=500; + const VALID_NORMALISATION_RELEASE_RANGE: RangeInclusive = 1..=1000; + const AP_PORT: &str = "ap-port"; const AUTOPLAY: &str = "autoplay"; const BACKEND: &str = "backend"; - const BITRATE: &str = "b"; - const CACHE: &str = "c"; + const BITRATE: &str = "bitrate"; + const CACHE: &str = "cache"; const CACHE_SIZE_LIMIT: &str = "cache-size-limit"; const DEVICE: &str = "device"; const DEVICE_TYPE: &str = "device-type"; @@ -210,7 +225,7 @@ fn get_setup(args: &[String]) -> Setup { const EMIT_SINK_EVENTS: &str = "emit-sink-events"; const ENABLE_VOLUME_NORMALISATION: &str = "enable-volume-normalisation"; const FORMAT: &str = "format"; - const HELP: &str = "h"; + const HELP: &str = "help"; const INITIAL_VOLUME: &str = "initial-volume"; const MIXER_TYPE: &str = "mixer"; const ALSA_MIXER_DEVICE: &str = "alsa-mixer-device"; @@ -228,6 +243,7 @@ fn get_setup(args: &[String]) -> Setup { const PASSTHROUGH: &str = "passthrough"; const PASSWORD: &str = "password"; const PROXY: &str = "proxy"; + const QUIET: &str = "quiet"; const SYSTEM_CACHE: &str = "system-cache"; const USERNAME: &str = "username"; const VERBOSE: &str = "verbose"; @@ -236,196 +252,331 @@ fn get_setup(args: &[String]) -> Setup { const VOLUME_RANGE: &str = "volume-range"; const ZEROCONF_PORT: &str = "zeroconf-port"; + // Mostly arbitrary. + const AUTOPLAY_SHORT: &str = "A"; + const AP_PORT_SHORT: &str = "a"; + const BACKEND_SHORT: &str = "B"; + const BITRATE_SHORT: &str = "b"; + const SYSTEM_CACHE_SHORT: &str = "C"; + const CACHE_SHORT: &str = "c"; + const DITHER_SHORT: &str = "D"; + const DEVICE_SHORT: &str = "d"; + const VOLUME_CTRL_SHORT: &str = "E"; + const VOLUME_RANGE_SHORT: &str = "e"; + const DEVICE_TYPE_SHORT: &str = "F"; + const FORMAT_SHORT: &str = "f"; + const DISABLE_AUDIO_CACHE_SHORT: &str = "G"; + const DISABLE_GAPLESS_SHORT: &str = "g"; + const DISABLE_CREDENTIAL_CACHE_SHORT: &str = "H"; + const HELP_SHORT: &str = "h"; + const CACHE_SIZE_LIMIT_SHORT: &str = "M"; + const MIXER_TYPE_SHORT: &str = "m"; + const ENABLE_VOLUME_NORMALISATION_SHORT: &str = "N"; + const NAME_SHORT: &str = "n"; + const DISABLE_DISCOVERY_SHORT: &str = "O"; + const ONEVENT_SHORT: &str = "o"; + const PASSTHROUGH_SHORT: &str = "P"; + const PASSWORD_SHORT: &str = "p"; + const EMIT_SINK_EVENTS_SHORT: &str = "Q"; + const QUIET_SHORT: &str = "q"; + const INITIAL_VOLUME_SHORT: &str = "R"; + const ALSA_MIXER_DEVICE_SHORT: &str = "S"; + const ALSA_MIXER_INDEX_SHORT: &str = "s"; + const ALSA_MIXER_CONTROL_SHORT: &str = "T"; + const NORMALISATION_ATTACK_SHORT: &str = "U"; + const USERNAME_SHORT: &str = "u"; + const VERSION_SHORT: &str = "V"; + const VERBOSE_SHORT: &str = "v"; + const NORMALISATION_GAIN_TYPE_SHORT: &str = "W"; + const NORMALISATION_KNEE_SHORT: &str = "w"; + const NORMALISATION_METHOD_SHORT: &str = "X"; + const PROXY_SHORT: &str = "x"; + const NORMALISATION_PREGAIN_SHORT: &str = "Y"; + const NORMALISATION_RELEASE_SHORT: &str = "y"; + const NORMALISATION_THRESHOLD_SHORT: &str = "Z"; + const ZEROCONF_PORT_SHORT: &str = "z"; + + // Options that have different desc's + // depending on what backends were enabled at build time. + #[cfg(feature = "alsa-backend")] + const MIXER_TYPE_DESC: &str = "Mixer to use {alsa|softvol}. Defaults to softvol."; + #[cfg(not(feature = "alsa-backend"))] + const MIXER_TYPE_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + ))] + const DEVICE_DESC: &str = "Audio device to use. Use ? to list options if using alsa, portaudio or rodio. Defaults to the backend's default."; + #[cfg(not(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + )))] + const DEVICE_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const ALSA_MIXER_CONTROL_DESC: &str = + "Alsa mixer control, e.g. PCM, Master or similar. Defaults to PCM."; + #[cfg(not(feature = "alsa-backend"))] + const ALSA_MIXER_CONTROL_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const ALSA_MIXER_DEVICE_DESC: &str = "Alsa mixer device, e.g hw:0 or similar from `aplay -l`. Defaults to `--device` if specified, default otherwise."; + #[cfg(not(feature = "alsa-backend"))] + const ALSA_MIXER_DEVICE_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const ALSA_MIXER_INDEX_DESC: &str = "Alsa index of the cards mixer. Defaults to 0."; + #[cfg(not(feature = "alsa-backend"))] + const ALSA_MIXER_INDEX_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const INITIAL_VOLUME_DESC: &str = "Initial volume in % from 0 - 100. Default for softvol: 50. For the alsa mixer: the current volume."; + #[cfg(not(feature = "alsa-backend"))] + const INITIAL_VOLUME_DESC: &str = "Initial volume in % from 0 - 100. Defaults to 50."; + #[cfg(feature = "alsa-backend")] + const VOLUME_RANGE_DESC: &str = "Range of the volume control (dB) from 0.0 to 100.0. Default for softvol: 60.0. For the alsa mixer: what the control supports."; + #[cfg(not(feature = "alsa-backend"))] + const VOLUME_RANGE_DESC: &str = + "Range of the volume control (dB) from 0.0 to 100.0. Defaults to 60.0."; + let mut opts = getopts::Options::new(); opts.optflag( + HELP_SHORT, HELP, - "help", "Print this help menu.", - ).optopt( - CACHE, - "cache", - "Path to a directory where files will be cached.", - "PATH", - ).optopt( - "", - SYSTEM_CACHE, - "Path to a directory where system files (credentials, volume) will be cached. May be different from the cache option value.", - "PATH", - ).optopt( - "", - CACHE_SIZE_LIMIT, - "Limits the size of the cache for audio files.", - "SIZE" - ).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.") - .optflag("", DISABLE_CREDENTIAL_CACHE, "Disable caching of credentials.") - .optopt("n", NAME, "Device name.", "NAME") - .optopt("", DEVICE_TYPE, "Displayed device type. Defaults to 'Speaker'.", "TYPE") + ) + .optflag( + VERSION_SHORT, + VERSION, + "Display librespot version string.", + ) + .optflag( + VERBOSE_SHORT, + VERBOSE, + "Enable verbose log output.", + ) + .optflag( + QUIET_SHORT, + QUIET, + "Only log warning and error messages.", + ) + .optflag( + DISABLE_AUDIO_CACHE_SHORT, + DISABLE_AUDIO_CACHE, + "Disable caching of the audio data.", + ) + .optflag( + DISABLE_CREDENTIAL_CACHE_SHORT, + DISABLE_CREDENTIAL_CACHE, + "Disable caching of credentials.", + ) + .optflag( + DISABLE_DISCOVERY_SHORT, + DISABLE_DISCOVERY, + "Disable zeroconf discovery mode.", + ) + .optflag( + DISABLE_GAPLESS_SHORT, + DISABLE_GAPLESS, + "Disable gapless playback.", + ) + .optflag( + EMIT_SINK_EVENTS_SHORT, + EMIT_SINK_EVENTS, + "Run PROGRAM set by `--onevent` before the sink is opened and after it is closed.", + ) + .optflag( + AUTOPLAY_SHORT, + AUTOPLAY, + "Automatically play similar songs when your music ends.", + ) + .optflag( + PASSTHROUGH_SHORT, + PASSTHROUGH, + "Pass a raw stream to the output. Only works with the pipe and subprocess backends.", + ) + .optflag( + ENABLE_VOLUME_NORMALISATION_SHORT, + ENABLE_VOLUME_NORMALISATION, + "Play all tracks at approximately the same apparent volume.", + ) .optopt( + NAME_SHORT, + NAME, + "Device name. Defaults to Librespot.", + "NAME", + ) + .optopt( + BITRATE_SHORT, BITRATE, - "bitrate", "Bitrate (kbps) {96|160|320}. Defaults to 160.", "BITRATE", ) .optopt( - "", - ONEVENT, - "Run PROGRAM when a playback event occurs.", - "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", VERSION, "Display librespot version string.") - .optopt("u", USERNAME, "Username used to sign in with.", "USERNAME") - .optopt("p", PASSWORD, "Password used to sign in with.", "PASSWORD") - .optopt("", PROXY, "HTTP proxy to use when connecting.", "URL") - .optopt("", AP_PORT, "Connect to an AP with a specified port. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT") - .optflag("", DISABLE_DISCOVERY, "Disable zeroconf discovery mode.") - .optopt( - "", - BACKEND, - "Audio backend to use. Use '?' to list options.", - "NAME", - ) - .optopt( - "", - DEVICE, - "Audio device to use. Use '?' to list options if using alsa, portaudio or rodio. Defaults to the backend's default.", - "NAME", - ) - .optopt( - "", + FORMAT_SHORT, FORMAT, "Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.", "FORMAT", ) .optopt( - "", + DITHER_SHORT, DITHER, - "Specify the dither algorithm to use {none|gpdf|tpdf|tpdf_hp}. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", + "Specify the dither algorithm to use {none|gpdf|tpdf|tpdf_hp}. Defaults to tpdf for formats S16, S24, S24_3 and none for other formats.", "DITHER", ) - .optopt("m", MIXER_TYPE, "Mixer to use {alsa|softvol}. Defaults to softvol", "MIXER") .optopt( - "", - "mixer-name", // deprecated - "", - "", - ) - .optopt( - "", - ALSA_MIXER_CONTROL, - "Alsa mixer control, e.g. 'PCM', 'Master' or similar. Defaults to 'PCM'.", - "NAME", - ) - .optopt( - "", - "mixer-card", // deprecated - "", - "", - ) - .optopt( - "", - ALSA_MIXER_DEVICE, - "Alsa mixer device, e.g 'hw:0' or similar from `aplay -l`. Defaults to `--device` if specified, 'default' otherwise.", - "DEVICE", - ) - .optopt( - "", - "mixer-index", // deprecated - "", - "", - ) - .optopt( - "", - ALSA_MIXER_INDEX, - "Alsa index of the cards mixer. Defaults to 0.", - "NUMBER", - ) - .optopt( - "", - INITIAL_VOLUME, - "Initial volume in % from 0-100. Default for softvol: '50'. For the Alsa mixer: the current volume.", - "VOLUME", - ) - .optopt( - "", - ZEROCONF_PORT, - "The port the internal server advertises over zeroconf.", - "PORT", - ) - .optflag( - "", - ENABLE_VOLUME_NORMALISATION, - "Play all tracks at approximately the same apparent volume.", - ) - .optopt( - "", - NORMALISATION_METHOD, - "Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.", - "METHOD", - ) - .optopt( - "", - NORMALISATION_GAIN_TYPE, - "Specify the normalisation gain type to use {track|album|auto}. Defaults to auto.", + DEVICE_TYPE_SHORT, + DEVICE_TYPE, + "Displayed device type. Defaults to speaker.", "TYPE", ) .optopt( - "", - NORMALISATION_PREGAIN, - "Pregain (dB) applied by volume normalisation. Defaults to 0.", - "PREGAIN", + CACHE_SHORT, + CACHE, + "Path to a directory where files will be cached.", + "PATH", ) .optopt( - "", - NORMALISATION_THRESHOLD, - "Threshold (dBFS) at which the dynamic limiter engages to prevent clipping. Defaults to -2.0.", - "THRESHOLD", + SYSTEM_CACHE_SHORT, + SYSTEM_CACHE, + "Path to a directory where system files (credentials, volume) will be cached. May be different from the `--cache` option value.", + "PATH", ) .optopt( - "", - NORMALISATION_ATTACK, - "Attack time (ms) in which the dynamic limiter reduces gain. Defaults to 5.", - "TIME", + CACHE_SIZE_LIMIT_SHORT, + CACHE_SIZE_LIMIT, + "Limits the size of the cache for audio files. It's possible to use suffixes like K, M or G, e.g. 16G for example.", + "SIZE" ) .optopt( - "", - NORMALISATION_RELEASE, - "Release or decay time (ms) in which the dynamic limiter restores gain. Defaults to 100.", - "TIME", + BACKEND_SHORT, + BACKEND, + "Audio backend to use. Use ? to list options.", + "NAME", ) .optopt( - "", - NORMALISATION_KNEE, - "Knee steepness of the dynamic limiter. Defaults to 1.0.", - "KNEE", + USERNAME_SHORT, + USERNAME, + "Username used to sign in with.", + "USERNAME", ) .optopt( - "", + PASSWORD_SHORT, + PASSWORD, + "Password used to sign in with.", + "PASSWORD", + ) + .optopt( + ONEVENT_SHORT, + ONEVENT, + "Run PROGRAM when a playback event occurs.", + "PROGRAM", + ) + .optopt( + ALSA_MIXER_CONTROL_SHORT, + ALSA_MIXER_CONTROL, + ALSA_MIXER_CONTROL_DESC, + "NAME", + ) + .optopt( + ALSA_MIXER_DEVICE_SHORT, + ALSA_MIXER_DEVICE, + ALSA_MIXER_DEVICE_DESC, + "DEVICE", + ) + .optopt( + ALSA_MIXER_INDEX_SHORT, + ALSA_MIXER_INDEX, + ALSA_MIXER_INDEX_DESC, + "NUMBER", + ) + .optopt( + MIXER_TYPE_SHORT, + MIXER_TYPE, + MIXER_TYPE_DESC, + "MIXER", + ) + .optopt( + DEVICE_SHORT, + DEVICE, + DEVICE_DESC, + "NAME", + ) + .optopt( + INITIAL_VOLUME_SHORT, + INITIAL_VOLUME, + INITIAL_VOLUME_DESC, + "VOLUME", + ) + .optopt( + VOLUME_CTRL_SHORT, VOLUME_CTRL, "Volume control scale type {cubic|fixed|linear|log}. Defaults to log.", "VOLUME_CTRL" ) .optopt( - "", + VOLUME_RANGE_SHORT, VOLUME_RANGE, - "Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports.", + VOLUME_RANGE_DESC, "RANGE", ) - .optflag( - "", - AUTOPLAY, - "Automatically play similar songs when your music ends.", + .optopt( + NORMALISATION_METHOD_SHORT, + NORMALISATION_METHOD, + "Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.", + "METHOD", ) - .optflag( - "", - DISABLE_GAPLESS, - "Disable gapless playback.", + .optopt( + NORMALISATION_GAIN_TYPE_SHORT, + NORMALISATION_GAIN_TYPE, + "Specify the normalisation gain type to use {track|album|auto}. Defaults to auto.", + "TYPE", ) - .optflag( - "", - PASSTHROUGH, - "Pass a raw stream to the output. Only works with the pipe and subprocess backends.", + .optopt( + NORMALISATION_PREGAIN_SHORT, + NORMALISATION_PREGAIN, + "Pregain (dB) applied by volume normalisation from -10.0 to 10.0. Defaults to 0.0.", + "PREGAIN", + ) + .optopt( + NORMALISATION_THRESHOLD_SHORT, + NORMALISATION_THRESHOLD, + "Threshold (dBFS) at which point the dynamic limiter engages to prevent clipping from 0.0 to -10.0. Defaults to -2.0.", + "THRESHOLD", + ) + .optopt( + NORMALISATION_ATTACK_SHORT, + NORMALISATION_ATTACK, + "Attack time (ms) in which the dynamic limiter reduces gain from 1 to 500. Defaults to 5.", + "TIME", + ) + .optopt( + NORMALISATION_RELEASE_SHORT, + NORMALISATION_RELEASE, + "Release or decay time (ms) in which the dynamic limiter restores gain from 1 to 1000. Defaults to 100.", + "TIME", + ) + .optopt( + NORMALISATION_KNEE_SHORT, + NORMALISATION_KNEE, + "Knee steepness of the dynamic limiter from 0.0 to 2.0. Defaults to 1.0.", + "KNEE", + ) + .optopt( + ZEROCONF_PORT_SHORT, + ZEROCONF_PORT, + "The port the internal server advertises over zeroconf 1 - 65535. Ports <= 1024 may require root privileges.", + "PORT", + ) + .optopt( + PROXY_SHORT, + PROXY, + "HTTP proxy to use when connecting.", + "URL", + ) + .optopt( + AP_PORT_SHORT, + AP_PORT, + "Connect to an AP with a specified port 1 - 65535. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070.", + "PORT", ); let matches = match opts.parse(&args[1..]) { @@ -450,110 +601,216 @@ fn get_setup(args: &[String]) -> Setup { exit(0); } - let verbose = matches.opt_present(VERBOSE); - setup_logging(verbose); + setup_logging(matches.opt_present(QUIET), matches.opt_present(VERBOSE)); info!("{}", get_version_string()); + #[cfg(not(feature = "alsa-backend"))] + for a in &[ + MIXER_TYPE, + ALSA_MIXER_DEVICE, + ALSA_MIXER_INDEX, + ALSA_MIXER_CONTROL, + ] { + if matches.opt_present(a) { + warn!("Alsa specific options have no effect if the alsa backend is not enabled at build time."); + break; + } + } + let backend_name = matches.opt_str(BACKEND); if backend_name == Some("?".into()) { list_backends(); exit(0); } - let backend = audio_backend::find(backend_name).expect("Invalid backend"); + let backend = audio_backend::find(backend_name).unwrap_or_else(|| { + error!( + "Invalid `--{}` / `-{}`: {}", + BACKEND, + BACKEND_SHORT, + matches.opt_str(BACKEND).unwrap_or_default() + ); + list_backends(); + exit(1); + }); let format = matches .opt_str(FORMAT) .as_deref() - .map(|format| AudioFormat::from_str(format).expect("Invalid output format")) + .map(|format| { + AudioFormat::from_str(format).unwrap_or_else(|_| { + error!("Invalid `--{}` / `-{}`: {}", FORMAT, FORMAT_SHORT, format); + println!( + "Valid `--{}` / `-{}` values: F64, F32, S32, S24, S24_3, S16", + FORMAT, FORMAT_SHORT + ); + println!("Default: {:?}", AudioFormat::default()); + exit(1); + }) + }) .unwrap_or_default(); + #[cfg(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + ))] let device = matches.opt_str(DEVICE); + + #[cfg(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + ))] if device == Some("?".into()) { backend(device, format); exit(0); } + #[cfg(not(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + )))] + let device: Option = None; + + #[cfg(not(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + )))] + if matches.opt_present(DEVICE) { + warn!( + "The `--{}` / `-{}` option is not supported by the included audio backend(s), and has no effect.", + DEVICE, DEVICE_SHORT, + ); + } + + #[cfg(feature = "alsa-backend")] let mixer_type = matches.opt_str(MIXER_TYPE); - let mixer = mixer::find(mixer_type.as_deref()).expect("Invalid mixer"); + #[cfg(not(feature = "alsa-backend"))] + let mixer_type: Option = None; + + let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| { + error!( + "Invalid `--{}` / `-{}`: {}", + MIXER_TYPE, + MIXER_TYPE_SHORT, + matches.opt_str(MIXER_TYPE).unwrap_or_default() + ); + println!( + "Valid `--{}` / `-{}` values: alsa, softvol", + MIXER_TYPE, MIXER_TYPE_SHORT + ); + println!("Default: softvol"); + exit(1); + }); let mixer_config = { - let mixer_device = match matches.opt_str("mixer-card") { - Some(card) => { - warn!("--mixer-card is deprecated and will be removed in a future release."); - warn!("Please use --alsa-mixer-device instead."); - card - } - None => matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { - if let Some(ref device_name) = device { - device_name.to_string() - } else { - MixerConfig::default().device - } - }), - }; + let mixer_default_config = MixerConfig::default(); - let index = match matches.opt_str("mixer-index") { - Some(index) => { - warn!("--mixer-index is deprecated and will be removed in a future release."); - warn!("Please use --alsa-mixer-index instead."); - index - .parse::() - .expect("Mixer index is not a valid number") + #[cfg(feature = "alsa-backend")] + let device = matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { + if let Some(ref device_name) = device { + device_name.to_string() + } else { + mixer_default_config.device.clone() } - None => matches - .opt_str(ALSA_MIXER_INDEX) - .map(|index| { - index - .parse::() - .expect("Alsa mixer index is not a valid number") + }); + + #[cfg(not(feature = "alsa-backend"))] + let device = mixer_default_config.device; + + #[cfg(feature = "alsa-backend")] + let index = matches + .opt_str(ALSA_MIXER_INDEX) + .map(|index| { + index.parse::().unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + ALSA_MIXER_INDEX, ALSA_MIXER_INDEX_SHORT, index + ); + println!("Default: {}", mixer_default_config.index); + exit(1); }) - .unwrap_or(0), - }; + }) + .unwrap_or_else(|| mixer_default_config.index); - let control = match matches.opt_str("mixer-name") { - Some(name) => { - warn!("--mixer-name is deprecated and will be removed in a future release."); - warn!("Please use --alsa-mixer-control instead."); - name - } - None => matches - .opt_str(ALSA_MIXER_CONTROL) - .unwrap_or_else(|| MixerConfig::default().control), - }; + #[cfg(not(feature = "alsa-backend"))] + let index = mixer_default_config.index; - let mut volume_range = matches + #[cfg(feature = "alsa-backend")] + let control = matches + .opt_str(ALSA_MIXER_CONTROL) + .unwrap_or(mixer_default_config.control); + + #[cfg(not(feature = "alsa-backend"))] + let control = mixer_default_config.control; + + let volume_range = matches .opt_str(VOLUME_RANGE) - .map(|range| range.parse::().unwrap()) + .map(|range| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + VOLUME_RANGE, VOLUME_RANGE_SHORT, range + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + VOLUME_RANGE, + VOLUME_RANGE_SHORT, + VALID_VOLUME_RANGE.start(), + VALID_VOLUME_RANGE.end() + ); + #[cfg(feature = "alsa-backend")] + println!( + "Default: softvol - {}, alsa - what the control supports", + VolumeCtrl::DEFAULT_DB_RANGE + ); + #[cfg(not(feature = "alsa-backend"))] + println!("Default: {}", VolumeCtrl::DEFAULT_DB_RANGE); + }; + + let range = range.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_VOLUME_RANGE).contains(&range) { + on_error(); + exit(1); + } + + range + }) .unwrap_or_else(|| match mixer_type.as_deref() { #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => 0.0, // let Alsa query the control + Some(AlsaMixer::NAME) => 0.0, // let alsa query the control _ => VolumeCtrl::DEFAULT_DB_RANGE, }); - if volume_range < 0.0 { - // User might have specified range as minimum dB volume. - volume_range = -volume_range; - warn!( - "Please enter positive volume ranges only, assuming {:.2} dB", - volume_range - ); - } + let volume_ctrl = matches .opt_str(VOLUME_CTRL) .as_deref() .map(|volume_ctrl| { - VolumeCtrl::from_str_with_range(volume_ctrl, volume_range) - .expect("Invalid volume control type") + VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + VOLUME_CTRL, VOLUME_CTRL_SHORT, volume_ctrl + ); + println!( + "Valid `--{}` / `-{}` values: cubic, fixed, linear, log", + VOLUME_CTRL, VOLUME_CTRL + ); + println!("Default: log"); + exit(1); + }) }) - .unwrap_or_else(|| { - let mut volume_ctrl = VolumeCtrl::default(); - volume_ctrl.set_db_range(volume_range); - volume_ctrl - }); + .unwrap_or_else(|| VolumeCtrl::Log(volume_range)); MixerConfig { - device: mixer_device, + device, control, index, volume_ctrl, @@ -588,7 +845,10 @@ fn get_setup(args: &[String]) -> Setup { .map(parse_file_size) .map(|e| { e.unwrap_or_else(|e| { - eprintln!("Invalid argument passed as cache size limit: {}", e); + error!( + "Invalid `--{}` / `-{}`: {}", + CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT, e + ); exit(1); }) }) @@ -596,6 +856,13 @@ fn get_setup(args: &[String]) -> Setup { None }; + if audio_dir.is_none() && matches.opt_present(CACHE_SIZE_LIMIT) { + warn!( + "Without a `--{}` / `-{}` path, and/or if the `--{}` / `-{}` flag is set, `--{}` / `-{}` has no effect.", + CACHE, CACHE_SHORT, DISABLE_AUDIO_CACHE, DISABLE_AUDIO_CACHE_SHORT, CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT + ); + } + match Cache::new(cred_dir, volume_dir, audio_dir, limit) { Ok(cache) => Some(cache), Err(e) => { @@ -605,31 +872,6 @@ fn get_setup(args: &[String]) -> Setup { } }; - let initial_volume = matches - .opt_str(INITIAL_VOLUME) - .map(|initial_volume| { - let volume = initial_volume.parse::().unwrap(); - if volume > 100 { - error!("Initial volume must be in the range 0-100."); - // the cast will saturate, not necessary to take further action - } - (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 - }) - .or_else(|| match mixer_type.as_deref() { - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => None, - _ => cache.as_ref().and_then(Cache::volume), - }); - - let zeroconf_port = matches - .opt_str(ZEROCONF_PORT) - .map(|port| port.parse::().unwrap()) - .unwrap_or(0); - - let name = matches - .opt_str(NAME) - .unwrap_or_else(|| "Librespot".to_string()); - let credentials = { let cached_credentials = cache.as_ref().and_then(Cache::credentials); @@ -647,13 +889,131 @@ fn get_setup(args: &[String]) -> Setup { ) }; - if credentials.is_none() && matches.opt_present(DISABLE_DISCOVERY) { + let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY); + + if credentials.is_none() && !enable_discovery { error!("Credentials are required if discovery is disabled."); exit(1); } + if !enable_discovery && matches.opt_present(ZEROCONF_PORT) { + warn!( + "With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", + DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT + ); + } + + let zeroconf_port = if enable_discovery { + matches + .opt_str(ZEROCONF_PORT) + .map(|port| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + ZEROCONF_PORT, ZEROCONF_PORT_SHORT, port + ); + println!( + "Valid `--{}` / `-{}` values: 1 - 65535", + ZEROCONF_PORT, ZEROCONF_PORT_SHORT + ); + }; + + let port = port.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if port == 0 { + on_error(); + exit(1); + } + + port + }) + .unwrap_or(0) + } else { + 0 + }; + + let connect_config = { + let connect_default_config = ConnectConfig::default(); + + let name = matches + .opt_str(NAME) + .unwrap_or_else(|| connect_default_config.name.clone()); + + let initial_volume = matches + .opt_str(INITIAL_VOLUME) + .map(|initial_volume| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + INITIAL_VOLUME, INITIAL_VOLUME_SHORT, initial_volume + ); + println!( + "Valid `--{}` / `-{}` values: 0 - 100", + INITIAL_VOLUME, INITIAL_VOLUME_SHORT + ); + #[cfg(feature = "alsa-backend")] + println!( + "Default: {}, or the current value when the alsa mixer is used.", + connect_default_config.initial_volume.unwrap_or_default() + ); + #[cfg(not(feature = "alsa-backend"))] + println!( + "Default: {}", + connect_default_config.initial_volume.unwrap_or_default() + ); + }; + + let volume = initial_volume.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if volume > 100 { + on_error(); + exit(1); + } + + (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 + }) + .or_else(|| match mixer_type.as_deref() { + #[cfg(feature = "alsa-backend")] + Some(AlsaMixer::NAME) => None, + _ => cache.as_ref().and_then(Cache::volume), + }); + + let device_type = matches + .opt_str(DEVICE_TYPE) + .as_deref() + .map(|device_type| { + DeviceType::from_str(device_type).unwrap_or_else(|_| { + error!("Invalid `--{}` / `-{}`: {}", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); + println!("Valid `--{}` / `-{}` values: computer, tablet, smartphone, speaker, tv, avr, stb, audiodongle, \ + gameconsole, castaudio, castvideo, automobile, smartwatch, chromebook, carthing, homething", + DEVICE_TYPE, DEVICE_TYPE_SHORT + ); + println!("Default: speaker"); + exit(1); + }) + }) + .unwrap_or_default(); + + let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); + let autoplay = matches.opt_present(AUTOPLAY); + + ConnectConfig { + name, + device_type, + initial_volume, + has_volume_ctrl, + autoplay, + } + }; + let session_config = { - let device_id = device_id(&name); + let device_id = device_id(&connect_config.name); SessionConfig { user_agent: version::VERSION_STRING.to_string(), @@ -663,78 +1023,329 @@ fn get_setup(args: &[String]) -> Setup { match Url::parse(&s) { Ok(url) => { if url.host().is_none() || url.port_or_known_default().is_none() { - panic!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); + error!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); + exit(1); } if url.scheme() != "http" { - panic!("Only unsecure http:// proxies are supported"); + error!("Only unsecure http:// proxies are supported"); + exit(1); } + url }, - Err(err) => panic!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", err) + Err(e) => { + error!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", e); + exit(1); + } } }, ), ap_port: matches .opt_str(AP_PORT) - .map(|port| port.parse::().expect("Invalid port")), + .map(|port| { + let on_error = || { + error!("Invalid `--{}` / `-{}`: {}", AP_PORT, AP_PORT_SHORT, port); + println!("Valid `--{}` / `-{}` values: 1 - 65535", AP_PORT, AP_PORT_SHORT); + }; + + let port = port.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if port == 0 { + on_error(); + exit(1); + } + + port + }), } }; let player_config = { + let player_default_config = PlayerConfig::default(); + let bitrate = matches .opt_str(BITRATE) .as_deref() - .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) - .unwrap_or_default(); + .map(|bitrate| { + Bitrate::from_str(bitrate).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + BITRATE, BITRATE_SHORT, bitrate + ); + println!( + "Valid `--{}` / `-{}` values: 96, 160, 320", + BITRATE, BITRATE_SHORT + ); + println!("Default: 160"); + exit(1); + }) + }) + .unwrap_or(player_default_config.bitrate); let gapless = !matches.opt_present(DISABLE_GAPLESS); let normalisation = matches.opt_present(ENABLE_VOLUME_NORMALISATION); - let normalisation_method = matches - .opt_str(NORMALISATION_METHOD) - .as_deref() - .map(|method| { - NormalisationMethod::from_str(method).expect("Invalid normalisation method") - }) - .unwrap_or_default(); - let normalisation_type = matches - .opt_str(NORMALISATION_GAIN_TYPE) - .as_deref() - .map(|gain_type| { - NormalisationType::from_str(gain_type).expect("Invalid normalisation type") - }) - .unwrap_or_default(); - let normalisation_pregain = matches - .opt_str(NORMALISATION_PREGAIN) - .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) - .unwrap_or(PlayerConfig::default().normalisation_pregain); - let normalisation_threshold = matches - .opt_str(NORMALISATION_THRESHOLD) - .map(|threshold| { - db_to_ratio( - threshold - .parse::() - .expect("Invalid threshold float value"), - ) - }) - .unwrap_or(PlayerConfig::default().normalisation_threshold); - let normalisation_attack = matches - .opt_str(NORMALISATION_ATTACK) - .map(|attack| { - Duration::from_millis(attack.parse::().expect("Invalid attack value")) - }) - .unwrap_or(PlayerConfig::default().normalisation_attack); - let normalisation_release = matches - .opt_str(NORMALISATION_RELEASE) - .map(|release| { - Duration::from_millis(release.parse::().expect("Invalid release value")) - }) - .unwrap_or(PlayerConfig::default().normalisation_release); - let normalisation_knee = matches - .opt_str(NORMALISATION_KNEE) - .map(|knee| knee.parse::().expect("Invalid knee float value")) - .unwrap_or(PlayerConfig::default().normalisation_knee); + + let normalisation_method; + let normalisation_type; + let normalisation_pregain; + let normalisation_threshold; + let normalisation_attack; + let normalisation_release; + let normalisation_knee; + + if !normalisation { + for a in &[ + NORMALISATION_METHOD, + NORMALISATION_GAIN_TYPE, + NORMALISATION_PREGAIN, + NORMALISATION_THRESHOLD, + NORMALISATION_ATTACK, + NORMALISATION_RELEASE, + NORMALISATION_KNEE, + ] { + if matches.opt_present(a) { + warn!( + "Without the `--{}` / `-{}` flag normalisation options have no effect.", + ENABLE_VOLUME_NORMALISATION, ENABLE_VOLUME_NORMALISATION_SHORT, + ); + break; + } + } + + normalisation_method = player_default_config.normalisation_method; + normalisation_type = player_default_config.normalisation_type; + normalisation_pregain = player_default_config.normalisation_pregain; + normalisation_threshold = player_default_config.normalisation_threshold; + normalisation_attack = player_default_config.normalisation_attack; + normalisation_release = player_default_config.normalisation_release; + normalisation_knee = player_default_config.normalisation_knee; + } else { + normalisation_method = matches + .opt_str(NORMALISATION_METHOD) + .as_deref() + .map(|method| { + warn!( + "`--{}` / `-{}` will be deprecated in a future release.", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT + ); + + let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method + ); + println!( + "Valid `--{}` / `-{}` values: basic, dynamic", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT + ); + println!("Default: {:?}", player_default_config.normalisation_method); + exit(1); + }); + + if matches!(method, NormalisationMethod::Basic) { + warn!( + "`--{}` / `-{}` {:?} will be deprecated in a future release.", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method + ); + } + + method + }) + .unwrap_or(player_default_config.normalisation_method); + + normalisation_type = matches + .opt_str(NORMALISATION_GAIN_TYPE) + .as_deref() + .map(|gain_type| { + NormalisationType::from_str(gain_type).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, gain_type + ); + println!( + "Valid `--{}` / `-{}` values: track, album, auto", + NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, + ); + println!("Default: {:?}", player_default_config.normalisation_type); + exit(1); + }) + }) + .unwrap_or(player_default_config.normalisation_type); + + normalisation_pregain = matches + .opt_str(NORMALISATION_PREGAIN) + .map(|pregain| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_PREGAIN, NORMALISATION_PREGAIN_SHORT, pregain + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_PREGAIN, + NORMALISATION_PREGAIN_SHORT, + VALID_NORMALISATION_PREGAIN_RANGE.start(), + VALID_NORMALISATION_PREGAIN_RANGE.end() + ); + println!("Default: {}", player_default_config.normalisation_pregain); + }; + + let pregain = pregain.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_PREGAIN_RANGE).contains(&pregain) { + on_error(); + exit(1); + } + + pregain + }) + .unwrap_or(player_default_config.normalisation_pregain); + + normalisation_threshold = matches + .opt_str(NORMALISATION_THRESHOLD) + .map(|threshold| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_THRESHOLD, NORMALISATION_THRESHOLD_SHORT, threshold + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_THRESHOLD, + NORMALISATION_THRESHOLD_SHORT, + VALID_NORMALISATION_THRESHOLD_RANGE.start(), + VALID_NORMALISATION_THRESHOLD_RANGE.end() + ); + println!( + "Default: {}", + ratio_to_db(player_default_config.normalisation_threshold) + ); + }; + + let threshold = threshold.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_THRESHOLD_RANGE).contains(&threshold) { + on_error(); + exit(1); + } + + db_to_ratio(threshold) + }) + .unwrap_or(player_default_config.normalisation_threshold); + + normalisation_attack = matches + .opt_str(NORMALISATION_ATTACK) + .map(|attack| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_ATTACK, NORMALISATION_ATTACK_SHORT, attack + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_ATTACK, + NORMALISATION_ATTACK_SHORT, + VALID_NORMALISATION_ATTACK_RANGE.start(), + VALID_NORMALISATION_ATTACK_RANGE.end() + ); + println!( + "Default: {}", + player_default_config.normalisation_attack.as_millis() + ); + }; + + let attack = attack.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_ATTACK_RANGE).contains(&attack) { + on_error(); + exit(1); + } + + Duration::from_millis(attack) + }) + .unwrap_or(player_default_config.normalisation_attack); + + normalisation_release = matches + .opt_str(NORMALISATION_RELEASE) + .map(|release| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_RELEASE, NORMALISATION_RELEASE_SHORT, release + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_RELEASE, + NORMALISATION_RELEASE_SHORT, + VALID_NORMALISATION_RELEASE_RANGE.start(), + VALID_NORMALISATION_RELEASE_RANGE.end() + ); + println!( + "Default: {}", + player_default_config.normalisation_release.as_millis() + ); + }; + + let release = release.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_RELEASE_RANGE).contains(&release) { + on_error(); + exit(1); + } + + Duration::from_millis(release) + }) + .unwrap_or(player_default_config.normalisation_release); + + normalisation_knee = matches + .opt_str(NORMALISATION_KNEE) + .map(|knee| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_KNEE, NORMALISATION_KNEE_SHORT, knee + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_KNEE, + NORMALISATION_KNEE_SHORT, + VALID_NORMALISATION_KNEE_RANGE.start(), + VALID_NORMALISATION_KNEE_RANGE.end() + ); + println!("Default: {}", player_default_config.normalisation_knee); + }; + + let knee = knee.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_KNEE_RANGE).contains(&knee) { + on_error(); + exit(1); + } + + knee + }) + .unwrap_or(player_default_config.normalisation_knee); + } let ditherer_name = matches.opt_str(DITHER); let ditherer = match ditherer_name.as_deref() { @@ -742,15 +1353,32 @@ fn get_setup(args: &[String]) -> Setup { Some("none") => None, // explicitly set on command line Some(_) => { - if format == AudioFormat::F64 || format == AudioFormat::F32 { - unimplemented!("Dithering is not available on format {:?}", format); + if matches!(format, AudioFormat::F64 | AudioFormat::F32) { + error!("Dithering is not available with format: {:?}.", format); + exit(1); } - Some(dither::find_ditherer(ditherer_name).expect("Invalid ditherer")) + + Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { + error!( + "Invalid `--{}` / `-{}`: {}", + DITHER, + DITHER_SHORT, + matches.opt_str(DITHER).unwrap_or_default() + ); + println!( + "Valid `--{}` / `-{}` values: none, gpdf, tpdf, tpdf_hp", + DITHER, DITHER_SHORT + ); + println!( + "Default: tpdf for formats S16, S24, S24_3 and none for other formats" + ); + exit(1); + })) } // nothing set on command line => use default None => match format { AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { - PlayerConfig::default().ditherer + player_default_config.ditherer } _ => None, }, @@ -774,25 +1402,6 @@ fn get_setup(args: &[String]) -> Setup { } }; - let connect_config = { - let device_type = matches - .opt_str(DEVICE_TYPE) - .as_deref() - .map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type")) - .unwrap_or_default(); - let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); - let autoplay = matches.opt_present(AUTOPLAY); - - ConnectConfig { - name, - device_type, - initial_volume, - has_volume_ctrl, - autoplay, - } - }; - - let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY); let player_event_program = matches.opt_str(ONEVENT); let emit_sink_events = matches.opt_present(EMIT_SINK_EVENTS); From 3016d6fbdb867eeb9cff7aa19fe61fa29ea13b72 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 17 Nov 2021 21:15:35 -0600 Subject: [PATCH 46/95] Guard against tracks_len being zero to prevent 'index out of bounds: the len is 0 but the index is 0' https://github.com/librespot-org/librespot/issues/226#issuecomment-971642037 --- connect/src/spirc.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index d644e2b0..344f63b7 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1140,6 +1140,14 @@ impl SpircTask { fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> { let tracks_len = self.state.get_track().len(); + // Guard against tracks_len being zero to prevent + // 'index out of bounds: the len is 0 but the index is 0' + // https://github.com/librespot-org/librespot/issues/226#issuecomment-971642037 + if tracks_len == 0 { + warn!("No playable track found in state: {:?}", self.state); + return None; + } + let mut new_playlist_index = index as usize; if new_playlist_index >= tracks_len { From c006a2364452a83584afc7b50e9714c4c71d24c7 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Fri, 19 Nov 2021 16:45:13 -0600 Subject: [PATCH 47/95] Improve `--device ?` functionality for the alsa backend This makes `--device ?` only show compatible devices (ones that support 2 ch 44.1 Interleaved) and it shows what `librespot` format(s) they support. This should be more useful to users as the info maps directly to `librespot`'s `--device` and `--format` options. --- CHANGELOG.md | 1 + playback/src/audio_backend/alsa.rs | 86 +++++++++++++++++++++--------- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c480e03f..fb800c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [main] Enforce reasonable ranges for option values (breaking). - [main] Don't evaluate options that would otherwise have no effect. +- [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 9dd3ea0c..e572f953 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -80,6 +80,23 @@ impl From for SinkError { } } +impl From for Format { + fn from(f: AudioFormat) -> Format { + use AudioFormat::*; + match f { + F64 => Format::float64(), + F32 => Format::float(), + S32 => Format::s32(), + S24 => Format::s24(), + S16 => Format::s16(), + #[cfg(target_endian = "little")] + S24_3 => Format::S243LE, + #[cfg(target_endian = "big")] + S24_3 => Format::S243BE, + } + } +} + pub struct AlsaSink { pcm: Option, format: AudioFormat, @@ -87,20 +104,50 @@ pub struct AlsaSink { period_buffer: Vec, } -fn list_outputs() -> SinkResult<()> { - println!("Listing available Alsa outputs:"); - for t in &["pcm", "ctl", "hwdep"] { - println!("{} devices:", t); +fn list_compatible_devices() -> SinkResult<()> { + println!("\n\n\tCompatible alsa device(s):\n"); + println!("\t------------------------------------------------------\n"); - let i = HintIter::new_str(None, t).map_err(|_| AlsaError::Parsing)?; + let i = HintIter::new_str(None, "pcm").map_err(|_| AlsaError::Parsing)?; - for a in i { - if let Some(Direction::Playback) = a.direction { - // mimic aplay -L - let name = a.name.ok_or(AlsaError::Parsing)?; - let desc = a.desc.ok_or(AlsaError::Parsing)?; + for a in i { + if let Some(Direction::Playback) = a.direction { + let name = a.name.ok_or(AlsaError::Parsing)?; + let desc = a.desc.ok_or(AlsaError::Parsing)?; - println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t")); + if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { + if let Ok(hwp) = HwParams::any(&pcm) { + // Only show devices that support + // 2 ch 44.1 Interleaved. + if hwp.set_access(Access::RWInterleaved).is_ok() + && hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).is_ok() + && hwp.set_channels(NUM_CHANNELS as u32).is_ok() + { + println!("\tDevice:\n\n\t\t{}\n", name); + println!("\tDescription:\n\n\t\t{}\n", desc.replace("\n", "\n\t\t")); + + let mut supported_formats = vec![]; + + for f in &[ + AudioFormat::S16, + AudioFormat::S24, + AudioFormat::S24_3, + AudioFormat::S32, + AudioFormat::F32, + AudioFormat::F64, + ] { + if hwp.test_format(Format::from(*f)).is_ok() { + supported_formats.push(format!("{:?}", f)); + } + } + + println!( + "\tSupported Format(s):\n\n\t\t{}\n", + supported_formats.join(" ") + ); + println!("\t------------------------------------------------------\n"); + } + }; } } } @@ -114,19 +161,6 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> e, })?; - let alsa_format = match format { - AudioFormat::F64 => Format::float64(), - AudioFormat::F32 => Format::float(), - AudioFormat::S32 => Format::s32(), - AudioFormat::S24 => Format::s24(), - AudioFormat::S16 => Format::s16(), - - #[cfg(target_endian = "little")] - AudioFormat::S24_3 => Format::S243LE, - #[cfg(target_endian = "big")] - AudioFormat::S24_3 => Format::S243BE, - }; - let bytes_per_period = { let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?; @@ -136,6 +170,8 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> e, })?; + let alsa_format = Format::from(format); + hwp.set_format(alsa_format) .map_err(|e| AlsaError::UnsupportedFormat { device: dev_name.to_string(), @@ -194,7 +230,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> impl Open for AlsaSink { fn open(device: Option, format: AudioFormat) -> Self { let name = match device.as_deref() { - Some("?") => match list_outputs() { + Some("?") => match list_compatible_devices() { Ok(_) => { exit(0); } From bbd575ed23cf9e27a1b43007875568fba8458694 Mon Sep 17 00:00:00 2001 From: Tom Vincent Date: Fri, 26 Nov 2021 18:49:50 +0000 Subject: [PATCH 48/95] Harden systemd service, update restart policy (#888) --- CHANGELOG.md | 1 + contrib/librespot.service | 8 ++++---- contrib/librespot.user.service | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb800c00..7ffd99cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] Enforce reasonable ranges for option values (breaking). - [main] Don't evaluate options that would otherwise have no effect. - [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. +- [contrib] Hardened security of the systemd service units ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/contrib/librespot.service b/contrib/librespot.service index 76037c8c..2c92a149 100644 --- a/contrib/librespot.service +++ b/contrib/librespot.service @@ -2,12 +2,12 @@ Description=Librespot (an open source Spotify client) Documentation=https://github.com/librespot-org/librespot Documentation=https://github.com/librespot-org/librespot/wiki/Options -Requires=network-online.target -After=network-online.target +Wants=network.target sound.target +After=network.target sound.target [Service] -User=nobody -Group=audio +DynamicUser=yes +SupplementaryGroups=audio Restart=always RestartSec=10 ExecStart=/usr/bin/librespot --name "%p@%H" diff --git a/contrib/librespot.user.service b/contrib/librespot.user.service index a676dde0..36f7f8c9 100644 --- a/contrib/librespot.user.service +++ b/contrib/librespot.user.service @@ -2,6 +2,8 @@ Description=Librespot (an open source Spotify client) Documentation=https://github.com/librespot-org/librespot Documentation=https://github.com/librespot-org/librespot/wiki/Options +Wants=network.target sound.target +After=network.target sound.target [Service] Restart=always From 56585cabb6e4527ef3cd9f621a239d58550d42c7 Mon Sep 17 00:00:00 2001 From: Nick Botticelli Date: Tue, 21 Sep 2021 01:18:58 -0700 Subject: [PATCH 49/95] Add Google sign in credential to protobufs --- .../proto/spotify/login5/v3/credentials/credentials.proto | 5 +++++ protocol/proto/spotify/login5/v3/login5.proto | 1 + 2 files changed, 6 insertions(+) diff --git a/protocol/proto/spotify/login5/v3/credentials/credentials.proto b/protocol/proto/spotify/login5/v3/credentials/credentials.proto index defab249..c1f43953 100644 --- a/protocol/proto/spotify/login5/v3/credentials/credentials.proto +++ b/protocol/proto/spotify/login5/v3/credentials/credentials.proto @@ -46,3 +46,8 @@ message SamsungSignInCredential { string id_token = 3; string token_endpoint_url = 4; } + +message GoogleSignInCredential { + string auth_code = 1; + string redirect_uri = 2; +} diff --git a/protocol/proto/spotify/login5/v3/login5.proto b/protocol/proto/spotify/login5/v3/login5.proto index f10ada21..4b41dcb2 100644 --- a/protocol/proto/spotify/login5/v3/login5.proto +++ b/protocol/proto/spotify/login5/v3/login5.proto @@ -52,6 +52,7 @@ message LoginRequest { credentials.ParentChildCredential parent_child_credential = 105; credentials.AppleSignInCredential apple_sign_in_credential = 106; credentials.SamsungSignInCredential samsung_sign_in_credential = 107; + credentials.GoogleSignInCredential google_sign_in_credential = 108; } } From d19fd240746fe8991b1bafa1bb95a7d618c4a962 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 26 Nov 2021 23:21:27 +0100 Subject: [PATCH 50/95] Add spclient and HTTPS support * Change metadata to use spclient * Add support for HTTPS proxies * Start purging unwraps and using Result instead --- Cargo.lock | 162 ++++++++++++++++--- core/Cargo.toml | 7 +- core/src/apresolve.rs | 15 +- core/src/dealer/mod.rs | 4 +- core/src/http_client.rs | 57 +++++-- core/src/lib.rs | 7 +- core/src/mercury/types.rs | 11 +- core/src/session.rs | 7 + core/src/spclient.rs | 256 ++++++++++++++++++++++++++++++- core/src/token.rs | 10 +- metadata/Cargo.toml | 5 +- metadata/src/lib.rs | 122 +++++++++++---- playback/src/player.rs | 9 +- protocol/Cargo.toml | 4 +- protocol/build.rs | 8 + protocol/proto/canvaz-meta.proto | 14 ++ protocol/proto/canvaz.proto | 40 +++++ protocol/proto/connect.proto | 6 +- src/main.rs | 8 +- 19 files changed, 652 insertions(+), 100 deletions(-) create mode 100644 protocol/proto/canvaz-meta.proto create mode 100644 protocol/proto/canvaz.proto diff --git a/Cargo.lock b/Cargo.lock index 37cbae56..7eddf8df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aes" version = "0.6.0" @@ -82,9 +84,9 @@ checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" [[package]] name = "async-trait" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722" +checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" dependencies = [ "proc-macro2", "quote", @@ -162,9 +164,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" @@ -256,12 +258,28 @@ dependencies = [ "memchr", ] +[[package]] +name = "core-foundation" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +dependencies = [ + "core-foundation-sys 0.8.3", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "coreaudio-rs" version = "0.10.0" @@ -288,7 +306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8351ddf2aaa3c583fa388029f8b3d26f3c7035a20911fdd5f2e2ed7ab57dad25" dependencies = [ "alsa", - "core-foundation-sys", + "core-foundation-sys 0.6.2", "coreaudio-rs", "jack 0.6.6", "jni", @@ -326,6 +344,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "ct-logs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" +dependencies = [ + "sct", +] + [[package]] name = "ctr" version = "0.6.0" @@ -719,6 +746,25 @@ dependencies = [ "system-deps", ] +[[package]] +name = "h2" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.9.1" @@ -797,9 +843,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes", "fnv", @@ -819,9 +865,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" [[package]] name = "httpdate" @@ -837,20 +883,21 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.8" +version = "0.14.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3f71a7eea53a3f8257a7b4795373ff886397178cd634430ea94e12d7fe4fe34" +checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "httparse", "httpdate", "itoa", - "pin-project", + "pin-project-lite", "socket2", "tokio", "tower-service", @@ -869,8 +916,29 @@ dependencies = [ "headers", "http", "hyper", + "hyper-rustls", + "rustls-native-certs", "tokio", + "tokio-rustls", "tower-service", + "webpki", +] + +[[package]] +name = "hyper-rustls" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" +dependencies = [ + "ct-logs", + "futures-util", + "hyper", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "webpki", ] [[package]] @@ -1052,9 +1120,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.95" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" +checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119" [[package]] name = "libloading" @@ -1223,6 +1291,7 @@ dependencies = [ "httparse", "hyper", "hyper-proxy", + "hyper-rustls", "librespot-protocol", "log", "num", @@ -1280,10 +1349,12 @@ version = "0.2.0" dependencies = [ "async-trait", "byteorder", + "bytes", "librespot-core", "librespot-protocol", "log", "protobuf", + "thiserror", ] [[package]] @@ -1667,6 +1738,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + [[package]] name = "parking_lot" version = "0.11.1" @@ -1866,24 +1943,24 @@ dependencies = [ [[package]] name = "protobuf" -version = "2.23.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45604fc7a88158e7d514d8e22e14ac746081e7a70d7690074dd0029ee37458d6" +checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754" [[package]] name = "protobuf-codegen" -version = "2.23.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb87f342b585958c1c086313dbc468dcac3edf5e90362111c26d7a58127ac095" +checksum = "3df8c98c08bd4d6653c2dbae00bd68c1d1d82a360265a5b0bbc73d48c63cb853" dependencies = [ "protobuf", ] [[package]] name = "protobuf-codegen-pure" -version = "2.23.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca6e0e2f898f7856a6328650abc9b2df71b7c1a5f39be0800d19051ad0214b2" +checksum = "394a73e2a819405364df8d30042c0f1174737a763e0170497ec9d36f8a2ea8f7" dependencies = [ "protobuf", "protobuf-codegen", @@ -2045,6 +2122,18 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustls-native-certs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +dependencies = [ + "openssl-probe", + "rustls", + "schannel", + "security-framework", +] + [[package]] name = "ryu" version = "1.0.5" @@ -2060,6 +2149,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -2099,6 +2198,29 @@ dependencies = [ "version-compare", ] +[[package]] +name = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys 0.8.3", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +dependencies = [ + "core-foundation-sys 0.8.3", + "libc", +] + [[package]] name = "semver" version = "0.11.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index 3c239034..64467366 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -16,15 +16,16 @@ version = "0.2.0" aes = "0.6" base64 = "0.13" byteorder = "1.4" -bytes = "1.0" +bytes = "1" form_urlencoded = "1.0" futures-core = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "unstable", "sink"] } hmac = "0.11" httparse = "1.3" http = "0.2" -hyper = { version = "0.14", features = ["client", "tcp", "http1"] } -hyper-proxy = { version = "0.9.1", default-features = false } +hyper = { version = "0.14", features = ["client", "tcp", "http1", "http2"] } +hyper-proxy = { version = "0.9.1", default-features = false, features = ["rustls"] } +hyper-rustls = { version = "0.22", default-features = false, features = ["native-tokio"] } log = "0.4" num = "0.4" num-bigint = { version = "0.4", features = ["rand"] } diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 623c7cb3..d39c3101 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -6,14 +6,14 @@ use std::sync::atomic::{AtomicUsize, Ordering}; pub type SocketAddress = (String, u16); #[derive(Default)] -struct AccessPoints { +pub struct AccessPoints { accesspoint: Vec, dealer: Vec, spclient: Vec, } #[derive(Deserialize)] -struct ApResolveData { +pub struct ApResolveData { accesspoint: Vec, dealer: Vec, spclient: Vec, @@ -42,7 +42,7 @@ component! { impl ApResolver { // return a port if a proxy URL and/or a proxy port was specified. This is useful even when // there is no proxy, but firewalls only allow certain ports (e.g. 443 and not 4070). - fn port_config(&self) -> Option { + pub fn port_config(&self) -> Option { if self.session().config().proxy.is_some() || self.session().config().ap_port.is_some() { Some(self.session().config().ap_port.unwrap_or(443)) } else { @@ -54,9 +54,7 @@ impl ApResolver { data.into_iter() .filter_map(|ap| { let mut split = ap.rsplitn(2, ':'); - let port = split - .next() - .expect("rsplitn should not return empty iterator"); + let port = split.next()?; let host = split.next()?.to_owned(); let port: u16 = port.parse().ok()?; if let Some(p) = self.port_config() { @@ -69,12 +67,11 @@ impl ApResolver { .collect() } - async fn try_apresolve(&self) -> Result> { + pub async fn try_apresolve(&self) -> Result> { let req = Request::builder() .method("GET") .uri("http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient") - .body(Body::empty()) - .unwrap(); + .body(Body::empty())?; let body = self.session().http_client().request_body(req).await?; let data: ApResolveData = serde_json::from_slice(body.as_ref())?; diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index bca1ec20..ba1e68df 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -401,7 +401,7 @@ async fn connect( // Spawn a task that will forward messages from the channel to the websocket. let send_task = { - let shared = Arc::clone(&shared); + let shared = Arc::clone(shared); tokio::spawn(async move { let result = loop { @@ -450,7 +450,7 @@ async fn connect( }) }; - let shared = Arc::clone(&shared); + let shared = Arc::clone(shared); // A task that receives messages from the web socket. let receive_task = tokio::spawn(async { diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 5f8ef780..ab1366a8 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,12 +1,25 @@ -use hyper::client::HttpConnector; -use hyper::{Body, Client, Request, Response}; +use hyper::{Body, Client, Request, Response, StatusCode}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; +use hyper_rustls::HttpsConnector; +use thiserror::Error; use url::Url; pub struct HttpClient { proxy: Option, } +#[derive(Error, Debug)] +pub enum HttpClientError { + #[error("could not parse request: {0}")] + Parsing(#[from] http::uri::InvalidUri), + #[error("could not send request: {0}")] + Request(hyper::Error), + #[error("could not read response: {0}")] + Response(hyper::Error), + #[error("could not build proxy connector: {0}")] + ProxyBuilder(#[from] std::io::Error), +} + impl HttpClient { pub fn new(proxy: Option<&Url>) -> Self { Self { @@ -14,21 +27,41 @@ impl HttpClient { } } - pub async fn request(&self, req: Request) -> Result, hyper::Error> { - if let Some(url) = &self.proxy { - // Panic safety: all URLs are valid URIs - let uri = url.to_string().parse().unwrap(); + pub async fn request(&self, req: Request) -> Result, HttpClientError> { + let connector = HttpsConnector::with_native_roots(); + let uri = req.uri().clone(); + + let response = if let Some(url) = &self.proxy { + let uri = url.to_string().parse()?; let proxy = Proxy::new(Intercept::All, uri); - let connector = HttpConnector::new(); - let proxy_connector = ProxyConnector::from_proxy_unsecured(connector, proxy); - Client::builder().build(proxy_connector).request(req).await + let proxy_connector = ProxyConnector::from_proxy(connector, proxy)?; + + Client::builder() + .build(proxy_connector) + .request(req) + .await + .map_err(HttpClientError::Request) } else { - Client::new().request(req).await + Client::builder() + .build(connector) + .request(req) + .await + .map_err(HttpClientError::Request) + }; + + if let Ok(response) = &response { + if response.status() != StatusCode::OK { + debug!("{} returned status {}", uri, response.status()); + } } + + response } - pub async fn request_body(&self, req: Request) -> Result { + pub async fn request_body(&self, req: Request) -> Result { let response = self.request(req).await?; - hyper::body::to_bytes(response.into_body()).await + hyper::body::to_bytes(response.into_body()) + .await + .map_err(HttpClientError::Response) } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 9c92c235..c928f32b 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -7,7 +7,7 @@ use librespot_protocol as protocol; #[macro_use] mod component; -mod apresolve; +pub mod apresolve; pub mod audio_key; pub mod authentication; pub mod cache; @@ -24,9 +24,10 @@ pub mod packet; mod proxytunnel; pub mod session; mod socket; -mod spclient; +#[allow(dead_code)] +pub mod spclient; pub mod spotify_id; -mod token; +pub mod token; #[doc(hidden)] pub mod util; pub mod version; diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 1d6b5b15..007ffb38 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -1,6 +1,8 @@ use byteorder::{BigEndian, WriteBytesExt}; use protobuf::Message; +use std::fmt; use std::io::Write; +use thiserror::Error; use crate::packet::PacketType; use crate::protocol; @@ -28,9 +30,15 @@ pub struct MercuryResponse { pub payload: Vec>, } -#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)] pub struct MercuryError; +impl fmt::Display for MercuryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Mercury error") + } +} + impl ToString for MercuryMethod { fn to_string(&self) -> String { match *self { @@ -55,6 +63,7 @@ impl MercuryMethod { } impl MercuryRequest { + // TODO: change into Result and remove unwraps pub fn encode(&self, seq: &[u8]) -> Vec { let mut packet = Vec::new(); packet.write_u16::(seq.len() as u16).unwrap(); diff --git a/core/src/session.rs b/core/src/session.rs index 81975a80..f683960a 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -27,6 +27,7 @@ use crate::connection::{self, AuthenticationError}; use crate::http_client::HttpClient; use crate::mercury::MercuryManager; use crate::packet::PacketType; +use crate::spclient::SpClient; use crate::token::TokenProvider; #[derive(Debug, Error)] @@ -55,6 +56,7 @@ struct SessionInternal { audio_key: OnceCell, channel: OnceCell, mercury: OnceCell, + spclient: OnceCell, token_provider: OnceCell, cache: Option>, @@ -95,6 +97,7 @@ impl Session { audio_key: OnceCell::new(), channel: OnceCell::new(), mercury: OnceCell::new(), + spclient: OnceCell::new(), token_provider: OnceCell::new(), handle: tokio::runtime::Handle::current(), session_id, @@ -159,6 +162,10 @@ impl Session { .get_or_init(|| MercuryManager::new(self.weak())) } + pub fn spclient(&self) -> &SpClient { + self.0.spclient.get_or_init(|| SpClient::new(self.weak())) + } + pub fn token_provider(&self) -> &TokenProvider { self.0 .token_provider diff --git a/core/src/spclient.rs b/core/src/spclient.rs index eb7b3f0f..77585bb9 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -1 +1,255 @@ -// https://github.com/librespot-org/librespot-java/blob/27783e06f456f95228c5ac37acf2bff8c1a8a0c4/lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java +use crate::apresolve::SocketAddress; +use crate::http_client::HttpClientError; +use crate::mercury::MercuryError; +use crate::protocol; +use crate::spotify_id::SpotifyId; + +use hyper::header::InvalidHeaderValue; +use hyper::{Body, HeaderMap, Request}; +use rand::Rng; +use std::time::Duration; +use thiserror::Error; + +component! { + SpClient : SpClientInner { + accesspoint: Option = None, + strategy: RequestStrategy = RequestStrategy::default(), + } +} + +pub type SpClientResult = Result; + +#[derive(Error, Debug)] +pub enum SpClientError { + #[error("could not get authorization token")] + Token(#[from] MercuryError), + #[error("could not parse request: {0}")] + Parsing(#[from] http::Error), + #[error("could not complete request: {0}")] + Network(#[from] HttpClientError), +} + +impl From for SpClientError { + fn from(err: InvalidHeaderValue) -> Self { + Self::Parsing(err.into()) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum RequestStrategy { + TryTimes(usize), + Infinitely, +} + +impl Default for RequestStrategy { + fn default() -> Self { + RequestStrategy::TryTimes(10) + } +} + +impl SpClient { + pub fn set_strategy(&self, strategy: RequestStrategy) { + self.lock(|inner| inner.strategy = strategy) + } + + pub async fn flush_accesspoint(&self) { + self.lock(|inner| inner.accesspoint = None) + } + + pub async fn get_accesspoint(&self) -> SocketAddress { + // Memoize the current access point. + let ap = self.lock(|inner| inner.accesspoint.clone()); + match ap { + Some(tuple) => tuple, + None => { + let tuple = self.session().apresolver().resolve("spclient").await; + self.lock(|inner| inner.accesspoint = Some(tuple.clone())); + info!( + "Resolved \"{}:{}\" as spclient access point", + tuple.0, tuple.1 + ); + tuple + } + } + } + + pub async fn base_url(&self) -> String { + let ap = self.get_accesspoint().await; + format!("https://{}:{}", ap.0, ap.1) + } + + pub async fn protobuf_request( + &self, + method: &str, + endpoint: &str, + headers: Option, + message: &dyn protobuf::Message, + ) -> SpClientResult { + let body = protobuf::text_format::print_to_string(message); + + let mut headers = headers.unwrap_or_else(HeaderMap::new); + headers.insert("Content-Type", "application/protobuf".parse()?); + + self.request(method, endpoint, Some(headers), Some(body)) + .await + } + + pub async fn request( + &self, + method: &str, + endpoint: &str, + headers: Option, + body: Option, + ) -> SpClientResult { + let mut tries: usize = 0; + let mut last_response; + + let body = body.unwrap_or_else(String::new); + + loop { + tries += 1; + + // Reconnection logic: retrieve the endpoint every iteration, so we can try + // another access point when we are experiencing network issues (see below). + let mut uri = self.base_url().await; + uri.push_str(endpoint); + + let mut request = Request::builder() + .method(method) + .uri(uri) + .body(Body::from(body.clone()))?; + + // Reconnection logic: keep getting (cached) tokens because they might have expired. + let headers_mut = request.headers_mut(); + if let Some(ref hdrs) = headers { + *headers_mut = hdrs.clone(); + } + headers_mut.insert( + "Authorization", + http::header::HeaderValue::from_str(&format!( + "Bearer {}", + self.session() + .token_provider() + .get_token("playlist-read") + .await? + .access_token + ))?, + ); + + last_response = self + .session() + .http_client() + .request_body(request) + .await + .map_err(SpClientError::Network); + if last_response.is_ok() { + return last_response; + } + + // Break before the reconnection logic below, so that the current access point + // is retained when max_tries == 1. Leave it up to the caller when to flush. + if let RequestStrategy::TryTimes(max_tries) = self.lock(|inner| inner.strategy) { + if tries >= max_tries { + break; + } + } + + // Reconnection logic: drop the current access point if we are experiencing issues. + // This will cause the next call to base_url() to resolve a new one. + if let Err(SpClientError::Network(ref network_error)) = last_response { + match network_error { + HttpClientError::Response(_) | HttpClientError::Request(_) => { + // Keep trying the current access point three times before dropping it. + if tries % 3 == 0 { + self.flush_accesspoint().await + } + } + _ => break, // if we can't build the request now, then we won't ever + } + } + + // When retrying, avoid hammering the Spotify infrastructure by sleeping a while. + // The backoff time is chosen randomly from an ever-increasing range. + let max_seconds = u64::pow(tries as u64, 2) * 3; + let backoff = Duration::from_secs(rand::thread_rng().gen_range(1..=max_seconds)); + warn!( + "Unable to complete API request, waiting {} seconds before retrying...", + backoff.as_secs(), + ); + debug!("Error was: {:?}", last_response); + tokio::time::sleep(backoff).await; + } + + last_response + } + + pub async fn put_connect_state( + &self, + connection_id: String, + state: protocol::connect::PutStateRequest, + ) -> SpClientResult { + let endpoint = format!("/connect-state/v1/devices/{}", self.session().device_id()); + + let mut headers = HeaderMap::new(); + headers.insert("X-Spotify-Connection-Id", connection_id.parse()?); + + self.protobuf_request("PUT", &endpoint, Some(headers), &state) + .await + } + + pub async fn get_metadata(&self, scope: &str, id: SpotifyId) -> SpClientResult { + let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()); + self.request("GET", &endpoint, None, None).await + } + + pub async fn get_track_metadata(&self, track_id: SpotifyId) -> SpClientResult { + self.get_metadata("track", track_id).await + } + + pub async fn get_episode_metadata(&self, episode_id: SpotifyId) -> SpClientResult { + self.get_metadata("episode", episode_id).await + } + + pub async fn get_album_metadata(&self, album_id: SpotifyId) -> SpClientResult { + self.get_metadata("album", album_id).await + } + + pub async fn get_artist_metadata(&self, artist_id: SpotifyId) -> SpClientResult { + self.get_metadata("artist", artist_id).await + } + + pub async fn get_show_metadata(&self, show_id: SpotifyId) -> SpClientResult { + self.get_metadata("show", show_id).await + } + + // TODO: Not working at the moment, always returns 400. + pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult { + // /color-lyrics/v2/track/22L7bfCiAkJo5xGSQgmiIO/image/spotify:image:ab67616d0000b273d9194aa18fa4c9362b47464f?clientLanguage=en + // https://spclient.wg.spotify.com/color-lyrics/v2/track/{track_id}/image/spotify:image:{image_id}?clientLanguage=en + let endpoint = format!("/color-lyrics/v2/track/{}", track_id.to_base16()); + + let mut headers = HeaderMap::new(); + headers.insert("Content-Type", "application/json".parse()?); + + self.request("GET", &endpoint, Some(headers), None).await + } + + // TODO: Find endpoint for newer canvas.proto and upgrade to that. + pub async fn get_canvases( + &self, + request: protocol::canvaz::EntityCanvazRequest, + ) -> SpClientResult { + let endpoint = "/canvaz-cache/v0/canvases"; + self.protobuf_request("POST", endpoint, None, &request) + .await + } + + pub async fn get_extended_metadata( + &self, + request: protocol::extended_metadata::BatchedEntityRequest, + ) -> SpClientResult { + let endpoint = "/extended-metadata/v0/extended-metadata"; + self.protobuf_request("POST", endpoint, None, &request) + .await + } +} diff --git a/core/src/token.rs b/core/src/token.rs index 824fcc3b..91a395fd 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -23,11 +23,11 @@ component! { #[derive(Clone, Debug)] pub struct Token { - access_token: String, - expires_in: Duration, - token_type: String, - scopes: Vec, - timestamp: Instant, + pub access_token: String, + pub expires_in: Duration, + pub token_type: String, + pub scopes: Vec, + pub timestamp: Instant, } #[derive(Deserialize)] diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 6e181a1a..9409bae6 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -10,12 +10,15 @@ edition = "2018" [dependencies] async-trait = "0.1" byteorder = "1.3" -protobuf = "2.14.0" +bytes = "1.0" log = "0.4" +protobuf = "2.14.0" +thiserror = "1" [dependencies.librespot-core] path = "../core" version = "0.2.0" + [dependencies.librespot-protocol] path = "../protocol" version = "0.2.0" diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index e7595f59..039bea83 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -12,9 +12,12 @@ use std::collections::HashMap; use librespot_core::mercury::MercuryError; use librespot_core::session::Session; +use librespot_core::spclient::SpClientError; use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId}; use librespot_protocol as protocol; -use protobuf::Message; +use protobuf::{Message, ProtobufError}; + +use thiserror::Error; pub use crate::protocol::metadata::AudioFile_Format as FileFormat; @@ -48,9 +51,8 @@ where } } - (has_forbidden || has_allowed) - && (!has_forbidden || !countrylist_contains(forbidden.as_str(), country)) - && (!has_allowed || countrylist_contains(allowed.as_str(), country)) + !(has_forbidden && countrylist_contains(forbidden.as_str(), country) + || has_allowed && !countrylist_contains(allowed.as_str(), country)) } // A wrapper with fields the player needs @@ -66,24 +68,34 @@ pub struct AudioItem { } impl AudioItem { - pub async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { + pub async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { match id.audio_type { SpotifyAudioType::Track => Track::get_audio_item(session, id).await, SpotifyAudioType::Podcast => Episode::get_audio_item(session, id).await, - SpotifyAudioType::NonPlayable => Err(MercuryError), + SpotifyAudioType::NonPlayable => Err(MetadataError::NonPlayable), } } } +pub type AudioItemResult = Result; + #[async_trait] trait AudioFiles { - async fn get_audio_item(session: &Session, id: SpotifyId) -> Result; + async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult; } #[async_trait] impl AudioFiles for Track { - async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { + async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult { let item = Self::get(session, id).await?; + let alternatives = { + if item.alternatives.is_empty() { + None + } else { + Some(item.alternatives) + } + }; + Ok(AudioItem { id, uri: format!("spotify:track:{}", id.to_base62()), @@ -91,14 +103,14 @@ impl AudioFiles for Track { name: item.name, duration: item.duration, available: item.available, - alternatives: Some(item.alternatives), + alternatives, }) } } #[async_trait] impl AudioFiles for Episode { - async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { + async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult { let item = Self::get(session, id).await?; Ok(AudioItem { @@ -113,23 +125,38 @@ impl AudioFiles for Episode { } } +#[derive(Debug, Error)] +pub enum MetadataError { + #[error("could not get metadata over HTTP: {0}")] + Http(#[from] SpClientError), + #[error("could not get metadata over Mercury: {0}")] + Mercury(#[from] MercuryError), + #[error("could not parse metadata: {0}")] + Parsing(#[from] ProtobufError), + #[error("response was empty")] + Empty, + #[error("audio item is non-playable")] + NonPlayable, +} + +pub type MetadataResult = Result; + #[async_trait] pub trait Metadata: Send + Sized + 'static { type Message: protobuf::Message; - fn request_url(id: SpotifyId) -> String; + async fn request(session: &Session, id: SpotifyId) -> MetadataResult; fn parse(msg: &Self::Message, session: &Session) -> Self; - async fn get(session: &Session, id: SpotifyId) -> Result { - let uri = Self::request_url(id); - let response = session.mercury().get(uri).await?; - let data = response.payload.first().expect("Empty payload"); - let msg = Self::Message::parse_from_bytes(data).unwrap(); - - Ok(Self::parse(&msg, &session)) + async fn get(session: &Session, id: SpotifyId) -> Result { + let response = Self::request(session, id).await?; + let msg = Self::Message::parse_from_bytes(&response)?; + Ok(Self::parse(&msg, session)) } } +// TODO: expose more fields available in the protobufs + #[derive(Debug, Clone)] pub struct Track { pub id: SpotifyId, @@ -189,14 +216,20 @@ pub struct Artist { pub top_tracks: Vec, } +#[async_trait] impl Metadata for Track { type Message = protocol::metadata::Track; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/track/{}", id.to_base16()) + async fn request(session: &Session, track_id: SpotifyId) -> MetadataResult { + session + .spclient() + .get_track_metadata(track_id) + .await + .map_err(MetadataError::Http) } fn parse(msg: &Self::Message, session: &Session) -> Self { + debug!("MESSAGE: {:?}", msg); let country = session.country(); let artists = msg @@ -234,11 +267,16 @@ impl Metadata for Track { } } +#[async_trait] impl Metadata for Album { type Message = protocol::metadata::Album; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/album/{}", id.to_base16()) + async fn request(session: &Session, album_id: SpotifyId) -> MetadataResult { + session + .spclient() + .get_album_metadata(album_id) + .await + .map_err(MetadataError::Http) } fn parse(msg: &Self::Message, _: &Session) -> Self { @@ -279,11 +317,20 @@ impl Metadata for Album { } } +#[async_trait] impl Metadata for Playlist { type Message = protocol::playlist4changes::SelectedListContent; - fn request_url(id: SpotifyId) -> String { - format!("hm://playlist/v2/playlist/{}", id.to_base62()) + // TODO: + // * Add PlaylistAnnotate3 annotations. + // * Find spclient endpoint and upgrade to that. + async fn request(session: &Session, playlist_id: SpotifyId) -> MetadataResult { + let uri = format!("hm://playlist/v2/playlist/{}", playlist_id.to_base62()); + let response = session.mercury().get(uri).await?; + match response.payload.first() { + Some(data) => Ok(data.to_vec().into()), + None => Err(MetadataError::Empty), + } } fn parse(msg: &Self::Message, _: &Session) -> Self { @@ -315,11 +362,16 @@ impl Metadata for Playlist { } } +#[async_trait] impl Metadata for Artist { type Message = protocol::metadata::Artist; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/artist/{}", id.to_base16()) + async fn request(session: &Session, artist_id: SpotifyId) -> MetadataResult { + session + .spclient() + .get_artist_metadata(artist_id) + .await + .map_err(MetadataError::Http) } fn parse(msg: &Self::Message, session: &Session) -> Self { @@ -348,11 +400,16 @@ impl Metadata for Artist { } // Podcast +#[async_trait] impl Metadata for Episode { type Message = protocol::metadata::Episode; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/episode/{}", id.to_base16()) + async fn request(session: &Session, episode_id: SpotifyId) -> MetadataResult { + session + .spclient() + .get_album_metadata(episode_id) + .await + .map_err(MetadataError::Http) } fn parse(msg: &Self::Message, session: &Session) -> Self { @@ -396,11 +453,16 @@ impl Metadata for Episode { } } +#[async_trait] impl Metadata for Show { type Message = protocol::metadata::Show; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/show/{}", id.to_base16()) + async fn request(session: &Session, show_id: SpotifyId) -> MetadataResult { + session + .spclient() + .get_show_metadata(show_id) + .await + .map_err(MetadataError::Http) } fn parse(msg: &Self::Message, _: &Session) -> Self { diff --git a/playback/src/player.rs b/playback/src/player.rs index 0249db9c..1395b99a 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -331,7 +331,11 @@ impl Player { // While PlayerInternal is written as a future, it still contains blocking code. // It must be run by using block_on() in a dedicated thread. - futures_executor::block_on(internal); + // futures_executor::block_on(internal); + + let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); + runtime.block_on(internal); + debug!("PlayerInternal thread finished."); }); @@ -1789,8 +1793,9 @@ impl PlayerInternal { let (result_tx, result_rx) = oneshot::channel(); + let handle = tokio::runtime::Handle::current(); std::thread::spawn(move || { - let data = futures_executor::block_on(loader.load_track(spotify_id, position_ms)); + let data = handle.block_on(loader.load_track(spotify_id, position_ms)); if let Some(data) = data { let _ = result_tx.send(data); } diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 5c3ae084..2628ecd1 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -9,8 +9,8 @@ repository = "https://github.com/librespot-org/librespot" edition = "2018" [dependencies] -protobuf = "2.14.0" +protobuf = "2.25" [build-dependencies] -protobuf-codegen-pure = "2.14.0" +protobuf-codegen-pure = "2.25" glob = "0.3.0" diff --git a/protocol/build.rs b/protocol/build.rs index 53e04bc7..37be7000 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -16,9 +16,17 @@ fn compile() { let proto_dir = Path::new(&env::var("CARGO_MANIFEST_DIR").expect("env")).join("proto"); let files = &[ + proto_dir.join("connect.proto"), + proto_dir.join("devices.proto"), + proto_dir.join("entity_extension_data.proto"), + proto_dir.join("extended_metadata.proto"), + proto_dir.join("extension_kind.proto"), proto_dir.join("metadata.proto"), + proto_dir.join("player.proto"), // TODO: remove these legacy protobufs when we are on the new API completely proto_dir.join("authentication.proto"), + proto_dir.join("canvaz.proto"), + proto_dir.join("canvaz-meta.proto"), proto_dir.join("keyexchange.proto"), proto_dir.join("mercury.proto"), proto_dir.join("playlist4changes.proto"), diff --git a/protocol/proto/canvaz-meta.proto b/protocol/proto/canvaz-meta.proto new file mode 100644 index 00000000..540daeb6 --- /dev/null +++ b/protocol/proto/canvaz-meta.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package com.spotify.canvaz; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.canvaz"; + +enum Type { + IMAGE = 0; + VIDEO = 1; + VIDEO_LOOPING = 2; + VIDEO_LOOPING_RANDOM = 3; + GIF = 4; +} \ No newline at end of file diff --git a/protocol/proto/canvaz.proto b/protocol/proto/canvaz.proto new file mode 100644 index 00000000..ca283ab5 --- /dev/null +++ b/protocol/proto/canvaz.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package com.spotify.canvazcache; + +import "canvaz-meta.proto"; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.canvaz"; + +message Artist { + string uri = 1; + string name = 2; + string avatar = 3; +} + +message EntityCanvazResponse { + repeated Canvaz canvases = 1; + message Canvaz { + string id = 1; + string url = 2; + string file_id = 3; + com.spotify.canvaz.Type type = 4; + string entity_uri = 5; + Artist artist = 6; + bool explicit = 7; + string uploaded_by = 8; + string etag = 9; + string canvas_uri = 11; + } + + int64 ttl_in_seconds = 2; +} + +message EntityCanvazRequest { + repeated Entity entities = 1; + message Entity { + string entity_uri = 1; + string etag = 2; + } +} \ No newline at end of file diff --git a/protocol/proto/connect.proto b/protocol/proto/connect.proto index 310a5b55..dae2561a 100644 --- a/protocol/proto/connect.proto +++ b/protocol/proto/connect.proto @@ -70,7 +70,7 @@ message DeviceInfo { Capabilities capabilities = 4; repeated DeviceMetadata metadata = 5; string device_software_version = 6; - devices.DeviceType device_type = 7; + spotify.connectstate.devices.DeviceType device_type = 7; string spirc_version = 9; string device_id = 10; bool is_private_session = 11; @@ -82,7 +82,7 @@ message DeviceInfo { string product_id = 17; string deduplication_id = 18; uint32 selected_alias_id = 19; - map device_aliases = 20; + map device_aliases = 20; bool is_offline = 21; string public_ip = 22; string license = 23; @@ -134,7 +134,7 @@ message Capabilities { bool supports_set_options_command = 25; CapabilitySupportDetails supports_hifi = 26; - reserved 1, 4, 24, "supported_contexts", "supports_lossless_audio"; + // reserved 1, 4, 24, "supported_contexts", "supports_lossless_audio"; } message CapabilitySupportDetails { diff --git a/src/main.rs b/src/main.rs index a3687aaa..185a9bf2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -606,15 +606,11 @@ fn get_setup(args: &[String]) -> Setup { match Url::parse(&s) { Ok(url) => { if url.host().is_none() || url.port_or_known_default().is_none() { - panic!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); - } - - if url.scheme() != "http" { - panic!("Only unsecure http:// proxies are supported"); + panic!("Invalid proxy url, only URLs on the format \"http(s)://host:port\" are allowed"); } url }, - Err(err) => panic!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", err) + Err(err) => panic!("Invalid proxy URL: {}, only URLs in the format \"http(s)://host:port\" are allowed", err) } }, ), From e1b273b8a1baffab22ce4a8cc5042fb6be9a3deb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Nov 2021 08:30:51 +0100 Subject: [PATCH 51/95] Fix lyrics retrieval --- core/src/http_client.rs | 29 ++++++++++++++++++++++++++--- core/src/spclient.rs | 37 +++++++++++++++++++------------------ 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/core/src/http_client.rs b/core/src/http_client.rs index ab1366a8..447c4e30 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,3 +1,7 @@ +use bytes::Bytes; +use http::header::HeaderValue; +use http::uri::InvalidUri; +use hyper::header::InvalidHeaderValue; use hyper::{Body, Client, Request, Response, StatusCode}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::HttpsConnector; @@ -11,7 +15,7 @@ pub struct HttpClient { #[derive(Error, Debug)] pub enum HttpClientError { #[error("could not parse request: {0}")] - Parsing(#[from] http::uri::InvalidUri), + Parsing(#[from] http::Error), #[error("could not send request: {0}")] Request(hyper::Error), #[error("could not read response: {0}")] @@ -20,6 +24,18 @@ pub enum HttpClientError { ProxyBuilder(#[from] std::io::Error), } +impl From for HttpClientError { + fn from(err: InvalidHeaderValue) -> Self { + Self::Parsing(err.into()) + } +} + +impl From for HttpClientError { + fn from(err: InvalidUri) -> Self { + Self::Parsing(err.into()) + } +} + impl HttpClient { pub fn new(proxy: Option<&Url>) -> Self { Self { @@ -27,10 +43,17 @@ impl HttpClient { } } - pub async fn request(&self, req: Request) -> Result, HttpClientError> { + pub async fn request(&self, mut req: Request) -> Result, HttpClientError> { let connector = HttpsConnector::with_native_roots(); let uri = req.uri().clone(); + let headers_mut = req.headers_mut(); + headers_mut.insert( + "User-Agent", + // Some features like lyrics are version-gated and require a "real" version string. + HeaderValue::from_str("Spotify/8.6.80 iOS/13.5 (iPhone11,2)")?, + ); + let response = if let Some(url) = &self.proxy { let uri = url.to_string().parse()?; let proxy = Proxy::new(Intercept::All, uri); @@ -58,7 +81,7 @@ impl HttpClient { response } - pub async fn request_body(&self, req: Request) -> Result { + pub async fn request_body(&self, req: Request) -> Result { let response = self.request(req).await?; hyper::body::to_bytes(response.into_body()) .await diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 77585bb9..686d3012 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -1,11 +1,16 @@ use crate::apresolve::SocketAddress; use crate::http_client::HttpClientError; use crate::mercury::MercuryError; -use crate::protocol; -use crate::spotify_id::SpotifyId; +use crate::protocol::canvaz::EntityCanvazRequest; +use crate::protocol::connect::PutStateRequest; +use crate::protocol::extended_metadata::BatchedEntityRequest; +use crate::spotify_id::{FileId, SpotifyId}; +use bytes::Bytes; +use http::header::HeaderValue; use hyper::header::InvalidHeaderValue; use hyper::{Body, HeaderMap, Request}; +use protobuf::Message; use rand::Rng; use std::time::Duration; use thiserror::Error; @@ -17,7 +22,7 @@ component! { } } -pub type SpClientResult = Result; +pub type SpClientResult = Result; #[derive(Error, Debug)] pub enum SpClientError { @@ -83,7 +88,7 @@ impl SpClient { method: &str, endpoint: &str, headers: Option, - message: &dyn protobuf::Message, + message: &dyn Message, ) -> SpClientResult { let body = protobuf::text_format::print_to_string(message); @@ -126,7 +131,7 @@ impl SpClient { } headers_mut.insert( "Authorization", - http::header::HeaderValue::from_str(&format!( + HeaderValue::from_str(&format!( "Bearer {}", self.session() .token_provider() @@ -186,7 +191,7 @@ impl SpClient { pub async fn put_connect_state( &self, connection_id: String, - state: protocol::connect::PutStateRequest, + state: PutStateRequest, ) -> SpClientResult { let endpoint = format!("/connect-state/v1/devices/{}", self.session().device_id()); @@ -223,10 +228,12 @@ impl SpClient { } // TODO: Not working at the moment, always returns 400. - pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult { - // /color-lyrics/v2/track/22L7bfCiAkJo5xGSQgmiIO/image/spotify:image:ab67616d0000b273d9194aa18fa4c9362b47464f?clientLanguage=en - // https://spclient.wg.spotify.com/color-lyrics/v2/track/{track_id}/image/spotify:image:{image_id}?clientLanguage=en - let endpoint = format!("/color-lyrics/v2/track/{}", track_id.to_base16()); + pub async fn get_lyrics(&self, track_id: SpotifyId, image_id: FileId) -> SpClientResult { + let endpoint = format!( + "/color-lyrics/v2/track/{}/image/spotify:image:{}", + track_id.to_base16(), + image_id + ); let mut headers = HeaderMap::new(); headers.insert("Content-Type", "application/json".parse()?); @@ -235,19 +242,13 @@ impl SpClient { } // TODO: Find endpoint for newer canvas.proto and upgrade to that. - pub async fn get_canvases( - &self, - request: protocol::canvaz::EntityCanvazRequest, - ) -> SpClientResult { + pub async fn get_canvases(&self, request: EntityCanvazRequest) -> SpClientResult { let endpoint = "/canvaz-cache/v0/canvases"; self.protobuf_request("POST", endpoint, None, &request) .await } - pub async fn get_extended_metadata( - &self, - request: protocol::extended_metadata::BatchedEntityRequest, - ) -> SpClientResult { + pub async fn get_extended_metadata(&self, request: BatchedEntityRequest) -> SpClientResult { let endpoint = "/extended-metadata/v0/extended-metadata"; self.protobuf_request("POST", endpoint, None, &request) .await From a73e05837e2e6a432556f21ba55b0f424983c3c1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Nov 2021 10:41:54 +0100 Subject: [PATCH 52/95] Return HttpClientError for status code <> 200 --- core/src/http_client.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 447c4e30..21a6c0a6 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -20,6 +20,8 @@ pub enum HttpClientError { Request(hyper::Error), #[error("could not read response: {0}")] Response(hyper::Error), + #[error("status code: {0}")] + NotOK(u16), #[error("could not build proxy connector: {0}")] ProxyBuilder(#[from] std::io::Error), } @@ -44,19 +46,20 @@ impl HttpClient { } pub async fn request(&self, mut req: Request) -> Result, HttpClientError> { + trace!("Requesting {:?}", req.uri().to_string()); + let connector = HttpsConnector::with_native_roots(); - let uri = req.uri().clone(); let headers_mut = req.headers_mut(); headers_mut.insert( "User-Agent", - // Some features like lyrics are version-gated and require a "real" version string. + // Some features like lyrics are version-gated and require an official version string. HeaderValue::from_str("Spotify/8.6.80 iOS/13.5 (iPhone11,2)")?, ); let response = if let Some(url) = &self.proxy { - let uri = url.to_string().parse()?; - let proxy = Proxy::new(Intercept::All, uri); + let proxy_uri = url.to_string().parse()?; + let proxy = Proxy::new(Intercept::All, proxy_uri); let proxy_connector = ProxyConnector::from_proxy(connector, proxy)?; Client::builder() @@ -73,8 +76,9 @@ impl HttpClient { }; if let Ok(response) = &response { - if response.status() != StatusCode::OK { - debug!("{} returned status {}", uri, response.status()); + let status = response.status(); + if status != StatusCode::OK { + return Err(HttpClientError::NotOK(status.into())); } } From f037a42908cb4fa65f91cc5934aa8fe17591fa93 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Nov 2021 11:59:22 +0100 Subject: [PATCH 53/95] Migrate and expand playlist protos --- metadata/src/lib.rs | 143 ++++++++++++++++++++++++-- protocol/build.rs | 7 +- protocol/proto/playlist4changes.proto | 87 ---------------- protocol/proto/playlist4content.proto | 37 ------- protocol/proto/playlist4issues.proto | 43 -------- protocol/proto/playlist4meta.proto | 52 ---------- protocol/proto/playlist4ops.proto | 103 ------------------- 7 files changed, 134 insertions(+), 338 deletions(-) delete mode 100644 protocol/proto/playlist4changes.proto delete mode 100644 protocol/proto/playlist4content.proto delete mode 100644 protocol/proto/playlist4issues.proto delete mode 100644 protocol/proto/playlist4meta.proto delete mode 100644 protocol/proto/playlist4ops.proto diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 039bea83..05ab028d 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -201,6 +201,21 @@ pub struct Show { pub covers: Vec, } +#[derive(Debug, Clone)] +pub struct TranscodedPicture { + pub target_name: String, + pub uri: String, +} + +#[derive(Debug, Clone)] +pub struct PlaylistAnnotation { + pub description: String, + pub picture: String, + pub transcoded_pictures: Vec, + pub abuse_reporting: bool, + pub taken_down: bool, +} + #[derive(Debug, Clone)] pub struct Playlist { pub revision: Vec, @@ -250,7 +265,7 @@ impl Metadata for Track { }) .collect(); - Track { + Self { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), duration: msg.get_duration(), @@ -307,7 +322,7 @@ impl Metadata for Album { }) .collect::>(); - Album { + Self { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), artists, @@ -318,12 +333,73 @@ impl Metadata for Album { } #[async_trait] -impl Metadata for Playlist { - type Message = protocol::playlist4changes::SelectedListContent; +impl Metadata for PlaylistAnnotation { + type Message = protocol::playlist_annotate3::PlaylistAnnotation; + + async fn request(session: &Session, playlist_id: SpotifyId) -> MetadataResult { + let current_user = session.username(); + Self::request_for_user(session, current_user, playlist_id).await + } + + fn parse(msg: &Self::Message, _: &Session) -> Self { + let transcoded_pictures = msg + .get_transcoded_picture() + .iter() + .map(|picture| TranscodedPicture { + target_name: picture.get_target_name().to_string(), + uri: picture.get_uri().to_string(), + }) + .collect::>(); + + let taken_down = !matches!( + msg.get_abuse_report_state(), + protocol::playlist_annotate3::AbuseReportState::OK + ); + + Self { + description: msg.get_description().to_string(), + picture: msg.get_picture().to_string(), + transcoded_pictures, + abuse_reporting: msg.get_is_abuse_reporting_enabled(), + taken_down, + } + } +} + +impl PlaylistAnnotation { + async fn request_for_user( + session: &Session, + username: String, + playlist_id: SpotifyId, + ) -> MetadataResult { + let uri = format!( + "hm://playlist-annotate/v1/annotation/user/{}/playlist/{}", + username, + playlist_id.to_base62() + ); + let response = session.mercury().get(uri).await?; + match response.payload.first() { + Some(data) => Ok(data.to_vec().into()), + None => Err(MetadataError::Empty), + } + } + + #[allow(dead_code)] + async fn get_for_user( + session: &Session, + username: String, + playlist_id: SpotifyId, + ) -> Result { + let response = Self::request_for_user(session, username, playlist_id).await?; + let msg = ::Message::parse_from_bytes(&response)?; + Ok(Self::parse(&msg, session)) + } +} + +#[async_trait] +impl Metadata for Playlist { + type Message = protocol::playlist4_external::SelectedListContent; - // TODO: - // * Add PlaylistAnnotate3 annotations. - // * Find spclient endpoint and upgrade to that. async fn request(session: &Session, playlist_id: SpotifyId) -> MetadataResult { let uri = format!("hm://playlist/v2/playlist/{}", playlist_id.to_base62()); let response = session.mercury().get(uri).await?; @@ -353,7 +429,7 @@ impl Metadata for Playlist { ); } - Playlist { + Self { revision: msg.get_revision().to_vec(), name: msg.get_attributes().get_name().to_owned(), tracks, @@ -362,6 +438,51 @@ impl Metadata for Playlist { } } +impl Playlist { + async fn request_for_user( + session: &Session, + username: String, + playlist_id: SpotifyId, + ) -> MetadataResult { + let uri = format!( + "hm://playlist/user/{}/playlist/{}", + username, + playlist_id.to_base62() + ); + let response = session.mercury().get(uri).await?; + match response.payload.first() { + Some(data) => Ok(data.to_vec().into()), + None => Err(MetadataError::Empty), + } + } + + async fn request_root_for_user(session: &Session, username: String) -> MetadataResult { + let uri = format!("hm://playlist/user/{}/rootlist", username); + let response = session.mercury().get(uri).await?; + match response.payload.first() { + Some(data) => Ok(data.to_vec().into()), + None => Err(MetadataError::Empty), + } + } + #[allow(dead_code)] + async fn get_for_user( + session: &Session, + username: String, + playlist_id: SpotifyId, + ) -> Result { + let response = Self::request_for_user(session, username, playlist_id).await?; + let msg = ::Message::parse_from_bytes(&response)?; + Ok(Self::parse(&msg, session)) + } + + #[allow(dead_code)] + async fn get_root_for_user(session: &Session, username: String) -> Result { + let response = Self::request_root_for_user(session, username).await?; + let msg = ::Message::parse_from_bytes(&response)?; + Ok(Self::parse(&msg, session)) + } +} + #[async_trait] impl Metadata for Artist { type Message = protocol::metadata::Artist; @@ -391,7 +512,7 @@ impl Metadata for Artist { None => Vec::new(), }; - Artist { + Self { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), top_tracks, @@ -438,7 +559,7 @@ impl Metadata for Episode { }) .collect::>(); - Episode { + Self { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), external_url: msg.get_external_url().to_owned(), @@ -485,7 +606,7 @@ impl Metadata for Show { }) .collect::>(); - Show { + Self { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), publisher: msg.get_publisher().to_owned(), diff --git a/protocol/build.rs b/protocol/build.rs index 37be7000..560bbfea 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -23,17 +23,14 @@ fn compile() { proto_dir.join("extension_kind.proto"), proto_dir.join("metadata.proto"), proto_dir.join("player.proto"), + proto_dir.join("playlist_annotate3.proto"), + proto_dir.join("playlist4_external.proto"), // TODO: remove these legacy protobufs when we are on the new API completely proto_dir.join("authentication.proto"), proto_dir.join("canvaz.proto"), proto_dir.join("canvaz-meta.proto"), proto_dir.join("keyexchange.proto"), proto_dir.join("mercury.proto"), - proto_dir.join("playlist4changes.proto"), - proto_dir.join("playlist4content.proto"), - proto_dir.join("playlist4issues.proto"), - proto_dir.join("playlist4meta.proto"), - proto_dir.join("playlist4ops.proto"), proto_dir.join("pubsub.proto"), proto_dir.join("spirc.proto"), ]; diff --git a/protocol/proto/playlist4changes.proto b/protocol/proto/playlist4changes.proto deleted file mode 100644 index 6b424b71..00000000 --- a/protocol/proto/playlist4changes.proto +++ /dev/null @@ -1,87 +0,0 @@ -syntax = "proto2"; - -import "playlist4ops.proto"; -import "playlist4meta.proto"; -import "playlist4content.proto"; -import "playlist4issues.proto"; - -message ChangeInfo { - optional string user = 0x1; - optional int32 timestamp = 0x2; - optional bool admin = 0x3; - optional bool undo = 0x4; - optional bool redo = 0x5; - optional bool merge = 0x6; - optional bool compressed = 0x7; - optional bool migration = 0x8; -} - -message Delta { - optional bytes base_version = 0x1; - repeated Op ops = 0x2; - optional ChangeInfo info = 0x4; -} - -message Merge { - optional bytes base_version = 0x1; - optional bytes merge_version = 0x2; - optional ChangeInfo info = 0x4; -} - -message ChangeSet { - optional Kind kind = 0x1; - enum Kind { - KIND_UNKNOWN = 0x0; - DELTA = 0x2; - MERGE = 0x3; - } - optional Delta delta = 0x2; - optional Merge merge = 0x3; -} - -message RevisionTaggedChangeSet { - optional bytes revision = 0x1; - optional ChangeSet change_set = 0x2; -} - -message Diff { - optional bytes from_revision = 0x1; - repeated Op ops = 0x2; - optional bytes to_revision = 0x3; -} - -message ListDump { - optional bytes latestRevision = 0x1; - optional int32 length = 0x2; - optional ListAttributes attributes = 0x3; - optional ListChecksum checksum = 0x4; - optional ListItems contents = 0x5; - repeated Delta pendingDeltas = 0x7; -} - -message ListChanges { - optional bytes baseRevision = 0x1; - repeated Delta deltas = 0x2; - optional bool wantResultingRevisions = 0x3; - optional bool wantSyncResult = 0x4; - optional ListDump dump = 0x5; - repeated int32 nonces = 0x6; -} - -message SelectedListContent { - optional bytes revision = 0x1; - optional int32 length = 0x2; - optional ListAttributes attributes = 0x3; - optional ListChecksum checksum = 0x4; - optional ListItems contents = 0x5; - optional Diff diff = 0x6; - optional Diff syncResult = 0x7; - repeated bytes resultingRevisions = 0x8; - optional bool multipleHeads = 0x9; - optional bool upToDate = 0xa; - repeated ClientResolveAction resolveAction = 0xc; - repeated ClientIssue issues = 0xd; - repeated int32 nonces = 0xe; - optional string owner_username =0x10; -} - diff --git a/protocol/proto/playlist4content.proto b/protocol/proto/playlist4content.proto deleted file mode 100644 index 50d197fa..00000000 --- a/protocol/proto/playlist4content.proto +++ /dev/null @@ -1,37 +0,0 @@ -syntax = "proto2"; - -import "playlist4meta.proto"; -import "playlist4issues.proto"; - -message Item { - optional string uri = 0x1; - optional ItemAttributes attributes = 0x2; -} - -message ListItems { - optional int32 pos = 0x1; - optional bool truncated = 0x2; - repeated Item items = 0x3; -} - -message ContentRange { - optional int32 pos = 0x1; - optional int32 length = 0x2; -} - -message ListContentSelection { - optional bool wantRevision = 0x1; - optional bool wantLength = 0x2; - optional bool wantAttributes = 0x3; - optional bool wantChecksum = 0x4; - optional bool wantContent = 0x5; - optional ContentRange contentRange = 0x6; - optional bool wantDiff = 0x7; - optional bytes baseRevision = 0x8; - optional bytes hintRevision = 0x9; - optional bool wantNothingIfUpToDate = 0xa; - optional bool wantResolveAction = 0xc; - repeated ClientIssue issues = 0xd; - repeated ClientResolveAction resolveAction = 0xe; -} - diff --git a/protocol/proto/playlist4issues.proto b/protocol/proto/playlist4issues.proto deleted file mode 100644 index 3808d532..00000000 --- a/protocol/proto/playlist4issues.proto +++ /dev/null @@ -1,43 +0,0 @@ -syntax = "proto2"; - -message ClientIssue { - optional Level level = 0x1; - enum Level { - LEVEL_UNKNOWN = 0x0; - LEVEL_DEBUG = 0x1; - LEVEL_INFO = 0x2; - LEVEL_NOTICE = 0x3; - LEVEL_WARNING = 0x4; - LEVEL_ERROR = 0x5; - } - optional Code code = 0x2; - enum Code { - CODE_UNKNOWN = 0x0; - CODE_INDEX_OUT_OF_BOUNDS = 0x1; - CODE_VERSION_MISMATCH = 0x2; - CODE_CACHED_CHANGE = 0x3; - CODE_OFFLINE_CHANGE = 0x4; - CODE_CONCURRENT_CHANGE = 0x5; - } - optional int32 repeatCount = 0x3; -} - -message ClientResolveAction { - optional Code code = 0x1; - enum Code { - CODE_UNKNOWN = 0x0; - CODE_NO_ACTION = 0x1; - CODE_RETRY = 0x2; - CODE_RELOAD = 0x3; - CODE_DISCARD_LOCAL_CHANGES = 0x4; - CODE_SEND_DUMP = 0x5; - CODE_DISPLAY_ERROR_MESSAGE = 0x6; - } - optional Initiator initiator = 0x2; - enum Initiator { - INITIATOR_UNKNOWN = 0x0; - INITIATOR_SERVER = 0x1; - INITIATOR_CLIENT = 0x2; - } -} - diff --git a/protocol/proto/playlist4meta.proto b/protocol/proto/playlist4meta.proto deleted file mode 100644 index 4c22a9f0..00000000 --- a/protocol/proto/playlist4meta.proto +++ /dev/null @@ -1,52 +0,0 @@ -syntax = "proto2"; - -message ListChecksum { - optional int32 version = 0x1; - optional bytes sha1 = 0x4; -} - -message DownloadFormat { - optional Codec codec = 0x1; - enum Codec { - CODEC_UNKNOWN = 0x0; - OGG_VORBIS = 0x1; - FLAC = 0x2; - MPEG_1_LAYER_3 = 0x3; - } -} - -message ListAttributes { - optional string name = 0x1; - optional string description = 0x2; - optional bytes picture = 0x3; - optional bool collaborative = 0x4; - optional string pl3_version = 0x5; - optional bool deleted_by_owner = 0x6; - optional bool restricted_collaborative = 0x7; - optional int64 deprecated_client_id = 0x8; - optional bool public_starred = 0x9; - optional string client_id = 0xa; -} - -message ItemAttributes { - optional string added_by = 0x1; - optional int64 timestamp = 0x2; - optional string message = 0x3; - optional bool seen = 0x4; - optional int64 download_count = 0x5; - optional DownloadFormat download_format = 0x6; - optional string sevendigital_id = 0x7; - optional int64 sevendigital_left = 0x8; - optional int64 seen_at = 0x9; - optional bool public = 0xa; -} - -message StringAttribute { - optional string key = 0x1; - optional string value = 0x2; -} - -message StringAttributes { - repeated StringAttribute attribute = 0x1; -} - diff --git a/protocol/proto/playlist4ops.proto b/protocol/proto/playlist4ops.proto deleted file mode 100644 index dbbfcaa9..00000000 --- a/protocol/proto/playlist4ops.proto +++ /dev/null @@ -1,103 +0,0 @@ -syntax = "proto2"; - -import "playlist4meta.proto"; -import "playlist4content.proto"; - -message Add { - optional int32 fromIndex = 0x1; - repeated Item items = 0x2; - optional ListChecksum list_checksum = 0x3; - optional bool addLast = 0x4; - optional bool addFirst = 0x5; -} - -message Rem { - optional int32 fromIndex = 0x1; - optional int32 length = 0x2; - repeated Item items = 0x3; - optional ListChecksum list_checksum = 0x4; - optional ListChecksum items_checksum = 0x5; - optional ListChecksum uris_checksum = 0x6; - optional bool itemsAsKey = 0x7; -} - -message Mov { - optional int32 fromIndex = 0x1; - optional int32 length = 0x2; - optional int32 toIndex = 0x3; - optional ListChecksum list_checksum = 0x4; - optional ListChecksum items_checksum = 0x5; - optional ListChecksum uris_checksum = 0x6; -} - -message ItemAttributesPartialState { - optional ItemAttributes values = 0x1; - repeated ItemAttributeKind no_value = 0x2; - - enum ItemAttributeKind { - ITEM_UNKNOWN = 0x0; - ITEM_ADDED_BY = 0x1; - ITEM_TIMESTAMP = 0x2; - ITEM_MESSAGE = 0x3; - ITEM_SEEN = 0x4; - ITEM_DOWNLOAD_COUNT = 0x5; - ITEM_DOWNLOAD_FORMAT = 0x6; - ITEM_SEVENDIGITAL_ID = 0x7; - ITEM_SEVENDIGITAL_LEFT = 0x8; - ITEM_SEEN_AT = 0x9; - ITEM_PUBLIC = 0xa; - } -} - -message ListAttributesPartialState { - optional ListAttributes values = 0x1; - repeated ListAttributeKind no_value = 0x2; - - enum ListAttributeKind { - LIST_UNKNOWN = 0x0; - LIST_NAME = 0x1; - LIST_DESCRIPTION = 0x2; - LIST_PICTURE = 0x3; - LIST_COLLABORATIVE = 0x4; - LIST_PL3_VERSION = 0x5; - LIST_DELETED_BY_OWNER = 0x6; - LIST_RESTRICTED_COLLABORATIVE = 0x7; - } -} - -message UpdateItemAttributes { - optional int32 index = 0x1; - optional ItemAttributesPartialState new_attributes = 0x2; - optional ItemAttributesPartialState old_attributes = 0x3; - optional ListChecksum list_checksum = 0x4; - optional ListChecksum old_attributes_checksum = 0x5; -} - -message UpdateListAttributes { - optional ListAttributesPartialState new_attributes = 0x1; - optional ListAttributesPartialState old_attributes = 0x2; - optional ListChecksum list_checksum = 0x3; - optional ListChecksum old_attributes_checksum = 0x4; -} - -message Op { - optional Kind kind = 0x1; - enum Kind { - KIND_UNKNOWN = 0x0; - ADD = 0x2; - REM = 0x3; - MOV = 0x4; - UPDATE_ITEM_ATTRIBUTES = 0x5; - UPDATE_LIST_ATTRIBUTES = 0x6; - } - optional Add add = 0x2; - optional Rem rem = 0x3; - optional Mov mov = 0x4; - optional UpdateItemAttributes update_item_attributes = 0x5; - optional UpdateListAttributes update_list_attributes = 0x6; -} - -message OpList { - repeated Op ops = 0x1; -} - From 47badd61e02e9d65b9e71e5bc04265c739faf58e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Nov 2021 14:26:13 +0100 Subject: [PATCH 54/95] Update tokio and fix build --- Cargo.lock | 24 ++-- playback/Cargo.toml | 2 +- playback/src/decoder/symphonia_decoder.rs | 136 ++++++++++++++++++++++ 3 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 playback/src/decoder/symphonia_decoder.rs diff --git a/Cargo.lock b/Cargo.lock index 7eddf8df..57e50c03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1808,18 +1808,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" +checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" +checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" dependencies = [ "proc-macro2", "quote", @@ -2498,9 +2498,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.6.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg", "bytes", @@ -2517,9 +2517,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.2.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" dependencies = [ "proc-macro2", "quote", @@ -2539,9 +2539,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" dependencies = [ "futures-core", "pin-project-lite", @@ -2567,9 +2567,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.7" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ "bytes", "futures-core", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 0bed793c..96b3649a 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -23,7 +23,7 @@ futures-util = { version = "0.3", default_features = false, features = ["alloc"] log = "0.4" byteorder = "1.4" shell-words = "1.0.0" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] } zerocopy = { version = "0.3" } # Backends diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs new file mode 100644 index 00000000..309c495d --- /dev/null +++ b/playback/src/decoder/symphonia_decoder.rs @@ -0,0 +1,136 @@ +use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; + +use crate::audio::AudioFile; + +use symphonia::core::audio::{AudioBufferRef, Channels}; +use symphonia::core::codecs::Decoder; +use symphonia::core::errors::Error as SymphoniaError; +use symphonia::core::formats::{FormatReader, SeekMode, SeekTo}; +use symphonia::core::io::{MediaSource, MediaSourceStream}; +use symphonia::core::units::TimeStamp; +use symphonia::default::{codecs::VorbisDecoder, formats::OggReader}; + +use std::io::{Read, Seek, SeekFrom}; + +impl MediaSource for FileWithConstSize +where + R: Read + Seek + Send, +{ + fn is_seekable(&self) -> bool { + true + } + + fn byte_len(&self) -> Option { + Some(self.len()) + } +} + +pub struct FileWithConstSize { + stream: T, + len: u64, +} + +impl FileWithConstSize { + pub fn len(&self) -> u64 { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl FileWithConstSize +where + T: Seek, +{ + pub fn new(mut stream: T) -> Self { + stream.seek(SeekFrom::End(0)).unwrap(); + let len = stream.stream_position().unwrap(); + stream.seek(SeekFrom::Start(0)).unwrap(); + Self { stream, len } + } +} + +impl Read for FileWithConstSize +where + T: Read, +{ + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.stream.read(buf) + } +} + +impl Seek for FileWithConstSize +where + T: Seek, +{ + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + self.stream.seek(pos) + } +} + +pub struct SymphoniaDecoder { + track_id: u32, + decoder: Box, + format: Box, + position: TimeStamp, +} + +impl SymphoniaDecoder { + pub fn new(input: R) -> DecoderResult + where + R: Read + Seek, + { + let mss_opts = Default::default(); + let mss = MediaSourceStream::new(Box::new(FileWithConstSize::new(input)), mss_opts); + + let format_opts = Default::default(); + let format = OggReader::try_new(mss, &format_opts).map_err(|e| DecoderError::SymphoniaDecoder(e.to_string()))?; + + let track = format.default_track().unwrap(); + let decoder_opts = Default::default(); + let decoder = VorbisDecoder::try_new(&track.codec_params, &decoder_opts)?; + + Ok(Self { + track_id: track.id, + decoder: Box::new(decoder), + format: Box::new(format), + position: 0, + }) + } +} + +impl AudioDecoder for SymphoniaDecoder { + fn seek(&mut self, absgp: u64) -> DecoderResult<()> { + let seeked_to = self.format.seek( + SeekMode::Accurate, + SeekTo::Time { + time: absgp, // TODO : move to Duration + track_id: Some(self.track_id), + }, + )?; + self.position = seeked_to.actual_ts; + // TODO : Ok(self.position) + Ok(()) + } + + fn next_packet(&mut self) -> DecoderResult> { + let packet = match self.format.next_packet() { + Ok(packet) => packet, + Err(e) => { + log::error!("format error: {}", err); + return Err(DecoderError::SymphoniaDecoder(e.to_string())), + } + }; + match self.decoder.decode(&packet) { + Ok(audio_buf) => { + self.position += packet.frames() as TimeStamp; + Ok(Some(packet)) + } + // TODO: Handle non-fatal decoding errors and retry. + Err(e) => + return Err(DecoderError::SymphoniaDecoder(e.to_string())), + } + } +} From e66cc5508cee0413829aa347c7a31bd0293eb856 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Wed, 1 Dec 2021 14:29:58 -0600 Subject: [PATCH 55/95] parse environment variables (#886) Make librespot able to parse environment variables for options and flags. To avoid name collisions environment variables must be prepended with `LIBRESPOT_` so option/flag `foo-bar` becomes `LIBRESPOT_FOO_BAR`. Verbose logging mode (`-v`, `--verbose`) logs all parsed environment variables and command line arguments (credentials are redacted). --- CHANGELOG.md | 2 + src/main.rs | 206 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 134 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ffd99cf..c5757aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] Don't evaluate options that would otherwise have no effect. - [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. - [contrib] Hardened security of the systemd service units +- [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). ### Added - [cache] Add `disable-credential-cache` flag (breaking). - [main] Use different option descriptions and error messages based on what backends are enabled at build time. - [main] Add a `-q`, `--quiet` option that changes the logging level to warn. - [main] Add a short name for every flag and option. +- [main] Add the ability to parse environment variables. ### Fixed - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. diff --git a/src/main.rs b/src/main.rs index 990de629..2dec56ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use futures_util::{future, FutureExt, StreamExt}; use librespot_playback::player::PlayerEvent; -use log::{error, info, warn}; +use log::{error, info, trace, warn}; use sha1::{Digest, Sha1}; use thiserror::Error; use tokio::sync::mpsc::UnboundedReceiver; @@ -44,6 +44,23 @@ fn usage(program: &str, opts: &getopts::Options) -> String { opts.usage(&brief) } +fn arg_to_var(arg: &str) -> String { + // To avoid name collisions environment variables must be prepended + // with `LIBRESPOT_` so option/flag `foo-bar` becomes `LIBRESPOT_FOO_BAR`. + format!("LIBRESPOT_{}", arg.to_uppercase().replace("-", "_")) +} + +fn env_var_present(arg: &str) -> bool { + env::var(arg_to_var(arg)).is_ok() +} + +fn env_var_opt_str(option: &str) -> Option { + match env::var(arg_to_var(option)) { + Ok(value) => Some(value), + Err(_) => None, + } +} + fn setup_logging(quiet: bool, verbose: bool) { let mut builder = env_logger::Builder::new(); match env::var("RUST_LOG") { @@ -591,20 +608,84 @@ fn get_setup(args: &[String]) -> Setup { } }; - if matches.opt_present(HELP) { + let opt_present = |opt| matches.opt_present(opt) || env_var_present(opt); + + let opt_str = |opt| { + if matches.opt_present(opt) { + matches.opt_str(opt) + } else { + env_var_opt_str(opt) + } + }; + + if opt_present(HELP) { println!("{}", usage(&args[0], &opts)); exit(0); } - if matches.opt_present(VERSION) { + if opt_present(VERSION) { println!("{}", get_version_string()); exit(0); } - setup_logging(matches.opt_present(QUIET), matches.opt_present(VERBOSE)); + setup_logging(opt_present(QUIET), opt_present(VERBOSE)); info!("{}", get_version_string()); + let librespot_env_vars: Vec = env::vars_os() + .filter_map(|(k, v)| { + let mut env_var = None; + if let Some(key) = k.to_str() { + if key.starts_with("LIBRESPOT_") { + if matches!(key, "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { + // Don't log creds. + env_var = Some(format!("\t\t{}=XXXXXXXX", key)); + } else if let Some(value) = v.to_str() { + env_var = Some(format!("\t\t{}={}", key, value)); + } + } + } + + env_var + }) + .collect(); + + if !librespot_env_vars.is_empty() { + trace!("Environment variable(s):"); + + for kv in librespot_env_vars { + trace!("{}", kv); + } + } + + let cmd_args = &args[1..]; + + let cmd_args_len = cmd_args.len(); + + if cmd_args_len > 0 { + trace!("Command line argument(s):"); + + for (index, key) in cmd_args.iter().enumerate() { + if key.starts_with('-') || key.starts_with("--") { + if matches!(key.as_str(), "--password" | "-p" | "--username" | "-u") { + // Don't log creds. + trace!("\t\t{} XXXXXXXX", key); + } else { + let mut value = "".to_string(); + let next = index + 1; + if next < cmd_args_len { + let next_key = cmd_args[next].clone(); + if !next_key.starts_with('-') && !next_key.starts_with("--") { + value = next_key; + } + } + + trace!("\t\t{} {}", key, value); + } + } + } + } + #[cfg(not(feature = "alsa-backend"))] for a in &[ MIXER_TYPE, @@ -612,13 +693,13 @@ fn get_setup(args: &[String]) -> Setup { ALSA_MIXER_INDEX, ALSA_MIXER_CONTROL, ] { - if matches.opt_present(a) { + if opt_present(a) { warn!("Alsa specific options have no effect if the alsa backend is not enabled at build time."); break; } } - let backend_name = matches.opt_str(BACKEND); + let backend_name = opt_str(BACKEND); if backend_name == Some("?".into()) { list_backends(); exit(0); @@ -629,14 +710,13 @@ fn get_setup(args: &[String]) -> Setup { "Invalid `--{}` / `-{}`: {}", BACKEND, BACKEND_SHORT, - matches.opt_str(BACKEND).unwrap_or_default() + opt_str(BACKEND).unwrap_or_default() ); list_backends(); exit(1); }); - let format = matches - .opt_str(FORMAT) + let format = opt_str(FORMAT) .as_deref() .map(|format| { AudioFormat::from_str(format).unwrap_or_else(|_| { @@ -656,7 +736,7 @@ fn get_setup(args: &[String]) -> Setup { feature = "rodio-backend", feature = "portaudio-backend" ))] - let device = matches.opt_str(DEVICE); + let device = opt_str(DEVICE); #[cfg(any( feature = "alsa-backend", @@ -680,7 +760,7 @@ fn get_setup(args: &[String]) -> Setup { feature = "rodio-backend", feature = "portaudio-backend" )))] - if matches.opt_present(DEVICE) { + if opt_present(DEVICE) { warn!( "The `--{}` / `-{}` option is not supported by the included audio backend(s), and has no effect.", DEVICE, DEVICE_SHORT, @@ -688,7 +768,7 @@ fn get_setup(args: &[String]) -> Setup { } #[cfg(feature = "alsa-backend")] - let mixer_type = matches.opt_str(MIXER_TYPE); + let mixer_type = opt_str(MIXER_TYPE); #[cfg(not(feature = "alsa-backend"))] let mixer_type: Option = None; @@ -697,7 +777,7 @@ fn get_setup(args: &[String]) -> Setup { "Invalid `--{}` / `-{}`: {}", MIXER_TYPE, MIXER_TYPE_SHORT, - matches.opt_str(MIXER_TYPE).unwrap_or_default() + opt_str(MIXER_TYPE).unwrap_or_default() ); println!( "Valid `--{}` / `-{}` values: alsa, softvol", @@ -711,7 +791,7 @@ fn get_setup(args: &[String]) -> Setup { let mixer_default_config = MixerConfig::default(); #[cfg(feature = "alsa-backend")] - let device = matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { + let device = opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { if let Some(ref device_name) = device { device_name.to_string() } else { @@ -723,8 +803,7 @@ fn get_setup(args: &[String]) -> Setup { let device = mixer_default_config.device; #[cfg(feature = "alsa-backend")] - let index = matches - .opt_str(ALSA_MIXER_INDEX) + let index = opt_str(ALSA_MIXER_INDEX) .map(|index| { index.parse::().unwrap_or_else(|_| { error!( @@ -741,15 +820,12 @@ fn get_setup(args: &[String]) -> Setup { let index = mixer_default_config.index; #[cfg(feature = "alsa-backend")] - let control = matches - .opt_str(ALSA_MIXER_CONTROL) - .unwrap_or(mixer_default_config.control); + let control = opt_str(ALSA_MIXER_CONTROL).unwrap_or(mixer_default_config.control); #[cfg(not(feature = "alsa-backend"))] let control = mixer_default_config.control; - let volume_range = matches - .opt_str(VOLUME_RANGE) + let volume_range = opt_str(VOLUME_RANGE) .map(|range| { let on_error = || { error!( @@ -790,8 +866,7 @@ fn get_setup(args: &[String]) -> Setup { _ => VolumeCtrl::DEFAULT_DB_RANGE, }); - let volume_ctrl = matches - .opt_str(VOLUME_CTRL) + let volume_ctrl = opt_str(VOLUME_CTRL) .as_deref() .map(|volume_ctrl| { VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { @@ -818,29 +893,26 @@ fn get_setup(args: &[String]) -> Setup { }; let cache = { - let volume_dir = matches - .opt_str(SYSTEM_CACHE) - .or_else(|| matches.opt_str(CACHE)) + let volume_dir = opt_str(SYSTEM_CACHE) + .or_else(|| opt_str(CACHE)) .map(|p| p.into()); - let cred_dir = if matches.opt_present(DISABLE_CREDENTIAL_CACHE) { + let cred_dir = if opt_present(DISABLE_CREDENTIAL_CACHE) { None } else { volume_dir.clone() }; - let audio_dir = if matches.opt_present(DISABLE_AUDIO_CACHE) { + let audio_dir = if opt_present(DISABLE_AUDIO_CACHE) { None } else { - matches - .opt_str(CACHE) + opt_str(CACHE) .as_ref() .map(|p| AsRef::::as_ref(p).join("files")) }; let limit = if audio_dir.is_some() { - matches - .opt_str(CACHE_SIZE_LIMIT) + opt_str(CACHE_SIZE_LIMIT) .as_deref() .map(parse_file_size) .map(|e| { @@ -856,7 +928,7 @@ fn get_setup(args: &[String]) -> Setup { None }; - if audio_dir.is_none() && matches.opt_present(CACHE_SIZE_LIMIT) { + if audio_dir.is_none() && opt_present(CACHE_SIZE_LIMIT) { warn!( "Without a `--{}` / `-{}` path, and/or if the `--{}` / `-{}` flag is set, `--{}` / `-{}` has no effect.", CACHE, CACHE_SHORT, DISABLE_AUDIO_CACHE, DISABLE_AUDIO_CACHE_SHORT, CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT @@ -882,21 +954,21 @@ fn get_setup(args: &[String]) -> Setup { }; get_credentials( - matches.opt_str(USERNAME), - matches.opt_str(PASSWORD), + opt_str(USERNAME), + opt_str(PASSWORD), cached_credentials, password, ) }; - let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY); + let enable_discovery = !opt_present(DISABLE_DISCOVERY); if credentials.is_none() && !enable_discovery { error!("Credentials are required if discovery is disabled."); exit(1); } - if !enable_discovery && matches.opt_present(ZEROCONF_PORT) { + if !enable_discovery && opt_present(ZEROCONF_PORT) { warn!( "With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT @@ -904,8 +976,7 @@ fn get_setup(args: &[String]) -> Setup { } let zeroconf_port = if enable_discovery { - matches - .opt_str(ZEROCONF_PORT) + opt_str(ZEROCONF_PORT) .map(|port| { let on_error = || { error!( @@ -938,12 +1009,9 @@ fn get_setup(args: &[String]) -> Setup { let connect_config = { let connect_default_config = ConnectConfig::default(); - let name = matches - .opt_str(NAME) - .unwrap_or_else(|| connect_default_config.name.clone()); + let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); - let initial_volume = matches - .opt_str(INITIAL_VOLUME) + let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { let on_error = || { error!( @@ -984,8 +1052,7 @@ fn get_setup(args: &[String]) -> Setup { _ => cache.as_ref().and_then(Cache::volume), }); - let device_type = matches - .opt_str(DEVICE_TYPE) + let device_type = opt_str(DEVICE_TYPE) .as_deref() .map(|device_type| { DeviceType::from_str(device_type).unwrap_or_else(|_| { @@ -1001,7 +1068,7 @@ fn get_setup(args: &[String]) -> Setup { .unwrap_or_default(); let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); - let autoplay = matches.opt_present(AUTOPLAY); + let autoplay = opt_present(AUTOPLAY); ConnectConfig { name, @@ -1018,7 +1085,7 @@ fn get_setup(args: &[String]) -> Setup { SessionConfig { user_agent: version::VERSION_STRING.to_string(), device_id, - proxy: matches.opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( + proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( |s| { match Url::parse(&s) { Ok(url) => { @@ -1041,8 +1108,7 @@ fn get_setup(args: &[String]) -> Setup { } }, ), - ap_port: matches - .opt_str(AP_PORT) + ap_port: opt_str(AP_PORT) .map(|port| { let on_error = || { error!("Invalid `--{}` / `-{}`: {}", AP_PORT, AP_PORT_SHORT, port); @@ -1067,8 +1133,7 @@ fn get_setup(args: &[String]) -> Setup { let player_config = { let player_default_config = PlayerConfig::default(); - let bitrate = matches - .opt_str(BITRATE) + let bitrate = opt_str(BITRATE) .as_deref() .map(|bitrate| { Bitrate::from_str(bitrate).unwrap_or_else(|_| { @@ -1086,9 +1151,9 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.bitrate); - let gapless = !matches.opt_present(DISABLE_GAPLESS); + let gapless = !opt_present(DISABLE_GAPLESS); - let normalisation = matches.opt_present(ENABLE_VOLUME_NORMALISATION); + let normalisation = opt_present(ENABLE_VOLUME_NORMALISATION); let normalisation_method; let normalisation_type; @@ -1108,7 +1173,7 @@ fn get_setup(args: &[String]) -> Setup { NORMALISATION_RELEASE, NORMALISATION_KNEE, ] { - if matches.opt_present(a) { + if opt_present(a) { warn!( "Without the `--{}` / `-{}` flag normalisation options have no effect.", ENABLE_VOLUME_NORMALISATION, ENABLE_VOLUME_NORMALISATION_SHORT, @@ -1125,8 +1190,7 @@ fn get_setup(args: &[String]) -> Setup { normalisation_release = player_default_config.normalisation_release; normalisation_knee = player_default_config.normalisation_knee; } else { - normalisation_method = matches - .opt_str(NORMALISATION_METHOD) + normalisation_method = opt_str(NORMALISATION_METHOD) .as_deref() .map(|method| { warn!( @@ -1158,8 +1222,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_method); - normalisation_type = matches - .opt_str(NORMALISATION_GAIN_TYPE) + normalisation_type = opt_str(NORMALISATION_GAIN_TYPE) .as_deref() .map(|gain_type| { NormalisationType::from_str(gain_type).unwrap_or_else(|_| { @@ -1177,8 +1240,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_type); - normalisation_pregain = matches - .opt_str(NORMALISATION_PREGAIN) + normalisation_pregain = opt_str(NORMALISATION_PREGAIN) .map(|pregain| { let on_error = || { error!( @@ -1209,8 +1271,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_pregain); - normalisation_threshold = matches - .opt_str(NORMALISATION_THRESHOLD) + normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) .map(|threshold| { let on_error = || { error!( @@ -1244,8 +1305,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_threshold); - normalisation_attack = matches - .opt_str(NORMALISATION_ATTACK) + normalisation_attack = opt_str(NORMALISATION_ATTACK) .map(|attack| { let on_error = || { error!( @@ -1279,8 +1339,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_attack); - normalisation_release = matches - .opt_str(NORMALISATION_RELEASE) + normalisation_release = opt_str(NORMALISATION_RELEASE) .map(|release| { let on_error = || { error!( @@ -1314,8 +1373,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_release); - normalisation_knee = matches - .opt_str(NORMALISATION_KNEE) + normalisation_knee = opt_str(NORMALISATION_KNEE) .map(|knee| { let on_error = || { error!( @@ -1347,7 +1405,7 @@ fn get_setup(args: &[String]) -> Setup { .unwrap_or(player_default_config.normalisation_knee); } - let ditherer_name = matches.opt_str(DITHER); + let ditherer_name = opt_str(DITHER); let ditherer = match ditherer_name.as_deref() { // explicitly disabled on command line Some("none") => None, @@ -1363,7 +1421,7 @@ fn get_setup(args: &[String]) -> Setup { "Invalid `--{}` / `-{}`: {}", DITHER, DITHER_SHORT, - matches.opt_str(DITHER).unwrap_or_default() + opt_str(DITHER).unwrap_or_default() ); println!( "Valid `--{}` / `-{}` values: none, gpdf, tpdf, tpdf_hp", @@ -1384,7 +1442,7 @@ fn get_setup(args: &[String]) -> Setup { }, }; - let passthrough = matches.opt_present(PASSTHROUGH); + let passthrough = opt_present(PASSTHROUGH); PlayerConfig { bitrate, @@ -1402,8 +1460,8 @@ fn get_setup(args: &[String]) -> Setup { } }; - let player_event_program = matches.opt_str(ONEVENT); - let emit_sink_events = matches.opt_present(EMIT_SINK_EVENTS); + let player_event_program = opt_str(ONEVENT); + let emit_sink_events = opt_present(EMIT_SINK_EVENTS); Setup { format, From 4370258716e3e3303b9242cda4ec894c80c0c31e Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Fri, 3 Dec 2021 11:47:51 -0600 Subject: [PATCH 56/95] Address clippy lint warnings for rust 1.57 --- connect/src/context.rs | 2 ++ core/src/connection/codec.rs | 3 +-- playback/src/audio_backend/jackaudio.rs | 9 +++------ playback/src/audio_backend/mod.rs | 2 +- playback/src/audio_backend/rodio.rs | 1 + playback/src/mixer/alsamixer.rs | 1 + 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/connect/src/context.rs b/connect/src/context.rs index 63a2aebb..154d9507 100644 --- a/connect/src/context.rs +++ b/connect/src/context.rs @@ -46,6 +46,7 @@ pub struct TrackContext { // pub metadata: MetadataContext, } +#[allow(dead_code)] #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ArtistContext { @@ -54,6 +55,7 @@ pub struct ArtistContext { image_uri: String, } +#[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct MetadataContext { album_title: String, diff --git a/core/src/connection/codec.rs b/core/src/connection/codec.rs index 299220f6..86533aaf 100644 --- a/core/src/connection/codec.rs +++ b/core/src/connection/codec.rs @@ -87,8 +87,7 @@ impl Decoder for ApCodec { let mut payload = buf.split_to(size + MAC_SIZE); - self.decode_cipher - .decrypt(&mut payload.get_mut(..size).unwrap()); + self.decode_cipher.decrypt(payload.get_mut(..size).unwrap()); let mac = payload.split_off(size); self.decode_cipher.check_mac(mac.as_ref())?; diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 5ba7b7ff..15acf99d 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -24,15 +24,12 @@ pub struct JackData { impl ProcessHandler for JackData { fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control { // get output port buffers - let mut out_r = self.port_r.as_mut_slice(ps); - let mut out_l = self.port_l.as_mut_slice(ps); - let buf_r: &mut [f32] = &mut out_r; - let buf_l: &mut [f32] = &mut out_l; + let buf_r: &mut [f32] = self.port_r.as_mut_slice(ps); + let buf_l: &mut [f32] = self.port_l.as_mut_slice(ps); // get queue iterator let mut queue_iter = self.rec.try_iter(); - let buf_size = buf_r.len(); - for i in 0..buf_size { + for i in 0..buf_r.len() { buf_r[i] = queue_iter.next().unwrap_or(0.0); buf_l[i] = queue_iter.next().unwrap_or(0.0); } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 4d3b0171..dc21fb3d 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -104,7 +104,7 @@ use self::gstreamer::GstreamerSink; #[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] mod rodio; -#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] +#[cfg(feature = "rodio-backend")] use self::rodio::RodioSink; #[cfg(feature = "sdl-backend")] diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 200c9fc4..ab356d67 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -227,5 +227,6 @@ impl Sink for RodioSink { } impl RodioSink { + #[allow(dead_code)] pub const NAME: &'static str = "rodio"; } diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 81d0436f..55398cb7 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -10,6 +10,7 @@ use alsa::{Ctl, Round}; use std::ffi::CString; #[derive(Clone)] +#[allow(dead_code)] pub struct AlsaMixer { config: MixerConfig, min: i64, From 0e2686863aa0746f2e329f7c2220fb779a83d8d1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 7 Dec 2021 23:22:24 +0100 Subject: [PATCH 57/95] Major metadata refactoring and enhancement * Expose all fields of recent protobufs * Add support for user-scoped playlists, user root playlists and playlist annotations * Convert messages with the Rust type system * Attempt to adhere to embargos (tracks and episodes scheduled for future release) * Return `Result`s with meaningful errors instead of panicking on `unwrap`s * Add foundation for future playlist editing * Up version in connection handshake to get all version-gated features --- Cargo.lock | 2 + connect/src/spirc.rs | 21 +- core/src/connection/handshake.rs | 2 +- core/src/spclient.rs | 1 - core/src/spotify_id.rs | 421 +++++++++++++++--- metadata/Cargo.toml | 2 + metadata/src/album.rs | 151 +++++++ metadata/src/artist.rs | 139 ++++++ metadata/src/audio/file.rs | 31 ++ metadata/src/audio/item.rs | 104 +++++ metadata/src/audio/mod.rs | 5 + metadata/src/availability.rs | 49 +++ metadata/src/content_rating.rs | 35 ++ metadata/src/copyright.rs | 37 ++ metadata/src/cover.rs | 20 - metadata/src/date.rs | 70 +++ metadata/src/episode.rs | 132 ++++++ metadata/src/error.rs | 34 ++ metadata/src/external_id.rs | 35 ++ metadata/src/image.rs | 103 +++++ metadata/src/lib.rs | 652 ++-------------------------- metadata/src/playlist/annotation.rs | 89 ++++ metadata/src/playlist/attribute.rs | 195 +++++++++ metadata/src/playlist/diff.rs | 29 ++ metadata/src/playlist/item.rs | 96 ++++ metadata/src/playlist/list.rs | 201 +++++++++ metadata/src/playlist/mod.rs | 9 + metadata/src/playlist/operation.rs | 114 +++++ metadata/src/request.rs | 20 + metadata/src/restriction.rs | 106 +++++ metadata/src/sale_period.rs | 37 ++ metadata/src/show.rs | 75 ++++ metadata/src/track.rs | 150 +++++++ metadata/src/util.rs | 39 ++ metadata/src/video.rs | 21 + playback/src/player.rs | 60 +-- 36 files changed, 2530 insertions(+), 757 deletions(-) create mode 100644 metadata/src/album.rs create mode 100644 metadata/src/artist.rs create mode 100644 metadata/src/audio/file.rs create mode 100644 metadata/src/audio/item.rs create mode 100644 metadata/src/audio/mod.rs create mode 100644 metadata/src/availability.rs create mode 100644 metadata/src/content_rating.rs create mode 100644 metadata/src/copyright.rs delete mode 100644 metadata/src/cover.rs create mode 100644 metadata/src/date.rs create mode 100644 metadata/src/episode.rs create mode 100644 metadata/src/error.rs create mode 100644 metadata/src/external_id.rs create mode 100644 metadata/src/image.rs create mode 100644 metadata/src/playlist/annotation.rs create mode 100644 metadata/src/playlist/attribute.rs create mode 100644 metadata/src/playlist/diff.rs create mode 100644 metadata/src/playlist/item.rs create mode 100644 metadata/src/playlist/list.rs create mode 100644 metadata/src/playlist/mod.rs create mode 100644 metadata/src/playlist/operation.rs create mode 100644 metadata/src/request.rs create mode 100644 metadata/src/restriction.rs create mode 100644 metadata/src/sale_period.rs create mode 100644 metadata/src/show.rs create mode 100644 metadata/src/track.rs create mode 100644 metadata/src/util.rs create mode 100644 metadata/src/video.rs diff --git a/Cargo.lock b/Cargo.lock index 57e50c03..1b537099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1350,11 +1350,13 @@ dependencies = [ "async-trait", "byteorder", "bytes", + "chrono", "librespot-core", "librespot-protocol", "log", "protobuf", "thiserror", + "uuid", ] [[package]] diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 57dc4cdd..e033b91d 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use std::future::Future; use std::pin::Pin; use std::time::{SystemTime, UNIX_EPOCH}; @@ -6,7 +7,7 @@ use crate::context::StationContext; use crate::core::config::ConnectConfig; use crate::core::mercury::{MercuryError, MercurySender}; use crate::core::session::Session; -use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError}; +use crate::core::spotify_id::SpotifyId; use crate::core::util::SeqGenerator; use crate::core::version; use crate::playback::mixer::Mixer; @@ -1099,15 +1100,6 @@ impl SpircTask { } } - // should this be a method of SpotifyId directly? - fn get_spotify_id_for_track(&self, track_ref: &TrackRef) -> Result { - SpotifyId::from_raw(track_ref.get_gid()).or_else(|_| { - let uri = track_ref.get_uri(); - debug!("Malformed or no gid, attempting to parse URI <{}>", uri); - SpotifyId::from_uri(uri) - }) - } - // Helper to find corresponding index(s) for track_id fn get_track_index_for_spotify_id( &self, @@ -1146,11 +1138,8 @@ impl SpircTask { // E.g - context based frames sometimes contain tracks with let mut track_ref = self.state.get_track()[new_playlist_index].clone(); - let mut track_id = self.get_spotify_id_for_track(&track_ref); - while self.track_ref_is_unavailable(&track_ref) - || track_id.is_err() - || track_id.unwrap().audio_type == SpotifyAudioType::NonPlayable - { + let mut track_id = SpotifyId::try_from(&track_ref); + while self.track_ref_is_unavailable(&track_ref) || track_id.is_err() { warn!( "Skipping track <{:?}> at position [{}] of {}", track_ref, new_playlist_index, tracks_len @@ -1166,7 +1155,7 @@ impl SpircTask { return None; } track_ref = self.state.get_track()[new_playlist_index].clone(); - track_id = self.get_spotify_id_for_track(&track_ref); + track_id = SpotifyId::try_from(&track_ref); } match track_id { diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 82ec7672..6b144ca0 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -49,7 +49,7 @@ where packet .mut_build_info() .set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86); - packet.mut_build_info().set_version(109800078); + packet.mut_build_info().set_version(999999999); packet .mut_cryptosuites_supported() .push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON); diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 686d3012..a3bfe9c5 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -227,7 +227,6 @@ impl SpClient { self.get_metadata("show", show_id).await } - // TODO: Not working at the moment, always returns 400. pub async fn get_lyrics(&self, track_id: SpotifyId, image_id: FileId) -> SpClientResult { let endpoint = format!( "/color-lyrics/v2/track/{}/image/spotify:image:{}", diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index e6e2bae0..c03382a2 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -1,31 +1,46 @@ -#![allow(clippy::wrong_self_convention)] +use librespot_protocol as protocol; -use std::convert::TryInto; +use thiserror::Error; + +use std::convert::{TryFrom, TryInto}; use std::fmt; +use std::ops::Deref; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SpotifyAudioType { +pub enum SpotifyItemType { + Album, + Artist, + Episode, + Playlist, + Show, Track, - Podcast, - NonPlayable, + Unknown, } -impl From<&str> for SpotifyAudioType { +impl From<&str> for SpotifyItemType { fn from(v: &str) -> Self { match v { - "track" => SpotifyAudioType::Track, - "episode" => SpotifyAudioType::Podcast, - _ => SpotifyAudioType::NonPlayable, + "album" => Self::Album, + "artist" => Self::Artist, + "episode" => Self::Episode, + "playlist" => Self::Playlist, + "show" => Self::Show, + "track" => Self::Track, + _ => Self::Unknown, } } } -impl From for &str { - fn from(audio_type: SpotifyAudioType) -> &'static str { - match audio_type { - SpotifyAudioType::Track => "track", - SpotifyAudioType::Podcast => "episode", - SpotifyAudioType::NonPlayable => "unknown", +impl From for &str { + fn from(item_type: SpotifyItemType) -> &'static str { + match item_type { + SpotifyItemType::Album => "album", + SpotifyItemType::Artist => "artist", + SpotifyItemType::Episode => "episode", + SpotifyItemType::Playlist => "playlist", + SpotifyItemType::Show => "show", + SpotifyItemType::Track => "track", + _ => "unknown", } } } @@ -33,11 +48,21 @@ impl From for &str { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SpotifyId { pub id: u128, - pub audio_type: SpotifyAudioType, + pub item_type: SpotifyItemType, } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct SpotifyIdError; +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] +pub enum SpotifyIdError { + #[error("ID cannot be parsed")] + InvalidId, + #[error("not a valid Spotify URI")] + InvalidFormat, + #[error("URI does not belong to Spotify")] + InvalidRoot, +} + +pub type SpotifyIdResult = Result; +pub type NamedSpotifyIdResult = Result; const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef"; @@ -47,11 +72,12 @@ impl SpotifyId { const SIZE_BASE16: usize = 32; const SIZE_BASE62: usize = 22; - fn track(n: u128) -> SpotifyId { - SpotifyId { - id: n, - audio_type: SpotifyAudioType::Track, - } + /// Returns whether this `SpotifyId` is for a playable audio item, if known. + pub fn is_playable(&self) -> bool { + return matches!( + self.item_type, + SpotifyItemType::Episode | SpotifyItemType::Track + ); } /// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`. @@ -59,29 +85,32 @@ impl SpotifyId { /// `src` is expected to be 32 bytes long and encoded using valid characters. /// /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - pub fn from_base16(src: &str) -> Result { + pub fn from_base16(src: &str) -> SpotifyIdResult { let mut dst: u128 = 0; for c in src.as_bytes() { let p = match c { b'0'..=b'9' => c - b'0', b'a'..=b'f' => c - b'a' + 10, - _ => return Err(SpotifyIdError), + _ => return Err(SpotifyIdError::InvalidId), } as u128; dst <<= 4; dst += p; } - Ok(SpotifyId::track(dst)) + Ok(Self { + id: dst, + item_type: SpotifyItemType::Unknown, + }) } - /// Parses a base62 encoded [Spotify ID] into a `SpotifyId`. + /// Parses a base62 encoded [Spotify ID] into a `u128`. /// /// `src` is expected to be 22 bytes long and encoded using valid characters. /// /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - pub fn from_base62(src: &str) -> Result { + pub fn from_base62(src: &str) -> SpotifyIdResult { let mut dst: u128 = 0; for c in src.as_bytes() { @@ -89,23 +118,29 @@ impl SpotifyId { b'0'..=b'9' => c - b'0', b'a'..=b'z' => c - b'a' + 10, b'A'..=b'Z' => c - b'A' + 36, - _ => return Err(SpotifyIdError), + _ => return Err(SpotifyIdError::InvalidId), } as u128; dst *= 62; dst += p; } - Ok(SpotifyId::track(dst)) + Ok(Self { + id: dst, + item_type: SpotifyItemType::Unknown, + }) } - /// Creates a `SpotifyId` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order. + /// Creates a `u128` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order. /// - /// The resulting `SpotifyId` will default to a `SpotifyAudioType::TRACK`. - pub fn from_raw(src: &[u8]) -> Result { + /// The resulting `SpotifyId` will default to a `SpotifyItemType::Unknown`. + pub fn from_raw(src: &[u8]) -> SpotifyIdResult { match src.try_into() { - Ok(dst) => Ok(SpotifyId::track(u128::from_be_bytes(dst))), - Err(_) => Err(SpotifyIdError), + Ok(dst) => Ok(Self { + id: u128::from_be_bytes(dst), + item_type: SpotifyItemType::Unknown, + }), + Err(_) => Err(SpotifyIdError::InvalidId), } } @@ -114,30 +149,37 @@ impl SpotifyId { /// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}` /// can be arbitrary while `{id}` is a 22-character long, base62 encoded Spotify ID. /// + /// Note that this should not be used for playlists, which have the form of + /// `spotify:user:{owner_username}:playlist:{id}`. + /// /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - pub fn from_uri(src: &str) -> Result { - let src = src.strip_prefix("spotify:").ok_or(SpotifyIdError)?; + pub fn from_uri(src: &str) -> SpotifyIdResult { + let mut uri_parts: Vec<&str> = src.split(':').collect(); - if src.len() <= SpotifyId::SIZE_BASE62 { - return Err(SpotifyIdError); + // At minimum, should be `spotify:{type}:{id}` + if uri_parts.len() < 3 { + return Err(SpotifyIdError::InvalidFormat); } - let colon_index = src.len() - SpotifyId::SIZE_BASE62 - 1; - - if src.as_bytes()[colon_index] != b':' { - return Err(SpotifyIdError); + if uri_parts[0] != "spotify" { + return Err(SpotifyIdError::InvalidRoot); } - let mut id = SpotifyId::from_base62(&src[colon_index + 1..])?; - id.audio_type = src[..colon_index].into(); + let id = uri_parts.pop().unwrap(); + if id.len() != Self::SIZE_BASE62 { + return Err(SpotifyIdError::InvalidId); + } - Ok(id) + Ok(Self { + item_type: uri_parts.pop().unwrap().into(), + ..Self::from_base62(id)? + }) } /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32) /// character long `String`. pub fn to_base16(&self) -> String { - to_base16(&self.to_raw(), &mut [0u8; SpotifyId::SIZE_BASE16]) + to_base16(&self.to_raw(), &mut [0u8; Self::SIZE_BASE16]) } /// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22) @@ -190,7 +232,7 @@ impl SpotifyId { /// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in /// big-endian order. - pub fn to_raw(&self) -> [u8; SpotifyId::SIZE] { + pub fn to_raw(&self) -> [u8; Self::SIZE] { self.id.to_be_bytes() } @@ -204,11 +246,11 @@ impl SpotifyId { /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids pub fn to_uri(&self) -> String { // 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31 - // + unknown size audio_type. - let audio_type: &str = self.audio_type.into(); - let mut dst = String::with_capacity(31 + audio_type.len()); + // + unknown size item_type. + let item_type: &str = self.item_type.into(); + let mut dst = String::with_capacity(31 + item_type.len()); dst.push_str("spotify:"); - dst.push_str(audio_type); + dst.push_str(item_type); dst.push(':'); dst.push_str(&self.to_base62()); @@ -216,10 +258,214 @@ impl SpotifyId { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct NamedSpotifyId { + pub inner_id: SpotifyId, + pub username: String, +} + +impl NamedSpotifyId { + pub fn from_uri(src: &str) -> NamedSpotifyIdResult { + let uri_parts: Vec<&str> = src.split(':').collect(); + + // At minimum, should be `spotify:user:{username}:{type}:{id}` + if uri_parts.len() < 5 { + return Err(SpotifyIdError::InvalidFormat); + } + + if uri_parts[0] != "spotify" { + return Err(SpotifyIdError::InvalidRoot); + } + + if uri_parts[1] != "user" { + return Err(SpotifyIdError::InvalidFormat); + } + + Ok(Self { + inner_id: SpotifyId::from_uri(src)?, + username: uri_parts[2].to_owned(), + }) + } + + pub fn to_uri(&self) -> String { + let item_type: &str = self.inner_id.item_type.into(); + let mut dst = String::with_capacity(37 + self.username.len() + item_type.len()); + dst.push_str("spotify:user:"); + dst.push_str(&self.username); + dst.push_str(item_type); + dst.push(':'); + dst.push_str(&self.to_base62()); + + dst + } + + pub fn from_spotify_id(id: SpotifyId, username: String) -> Self { + Self { + inner_id: id, + username, + } + } +} + +impl Deref for NamedSpotifyId { + type Target = SpotifyId; + fn deref(&self) -> &Self::Target { + &self.inner_id + } +} + +impl TryFrom<&[u8]> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(src: &[u8]) -> Result { + Self::from_raw(src) + } +} + +impl TryFrom<&str> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(src: &str) -> Result { + Self::from_base62(src) + } +} + +impl TryFrom for SpotifyId { + type Error = SpotifyIdError; + fn try_from(src: String) -> Result { + Self::try_from(src.as_str()) + } +} + +impl TryFrom<&Vec> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(src: &Vec) -> Result { + Self::try_from(src.as_slice()) + } +} + +impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(track: &protocol::spirc::TrackRef) -> Result { + match SpotifyId::from_raw(track.get_gid()) { + Ok(mut id) => { + id.item_type = SpotifyItemType::Track; + Ok(id) + } + Err(_) => SpotifyId::from_uri(track.get_uri()), + } + } +} + +impl TryFrom<&protocol::metadata::Album> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(album: &protocol::metadata::Album) -> Result { + Ok(Self { + item_type: SpotifyItemType::Album, + ..Self::from_raw(album.get_gid())? + }) + } +} + +impl TryFrom<&protocol::metadata::Artist> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(artist: &protocol::metadata::Artist) -> Result { + Ok(Self { + item_type: SpotifyItemType::Artist, + ..Self::from_raw(artist.get_gid())? + }) + } +} + +impl TryFrom<&protocol::metadata::Episode> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(episode: &protocol::metadata::Episode) -> Result { + Ok(Self { + item_type: SpotifyItemType::Episode, + ..Self::from_raw(episode.get_gid())? + }) + } +} + +impl TryFrom<&protocol::metadata::Track> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(track: &protocol::metadata::Track) -> Result { + Ok(Self { + item_type: SpotifyItemType::Track, + ..Self::from_raw(track.get_gid())? + }) + } +} + +impl TryFrom<&protocol::metadata::Show> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(show: &protocol::metadata::Show) -> Result { + Ok(Self { + item_type: SpotifyItemType::Show, + ..Self::from_raw(show.get_gid())? + }) + } +} + +impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result { + Ok(Self { + item_type: SpotifyItemType::Artist, + ..Self::from_raw(artist.get_artist_gid())? + }) + } +} + +impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(item: &protocol::playlist4_external::Item) -> Result { + Ok(Self { + item_type: SpotifyItemType::Track, + ..Self::from_uri(item.get_uri())? + }) + } +} + +// Note that this is the unique revision of an item's metadata on a playlist, +// not the ID of that item or playlist. +impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId { + type Error = SpotifyIdError; + fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result { + Self::try_from(item.get_revision()) + } +} + +// Note that this is the unique revision of a playlist, not the ID of that playlist. +impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId { + type Error = SpotifyIdError; + fn try_from( + playlist: &protocol::playlist4_external::SelectedListContent, + ) -> Result { + Self::try_from(playlist.get_revision()) + } +} + +// TODO: check meaning and format of this field in the wild. This might be a FileId, +// which is why we now don't create a separate `Playlist` enum value yet and choose +// to discard any item type. +impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId { + type Error = SpotifyIdError; + fn try_from( + picture: &protocol::playlist_annotate3::TranscodedPicture, + ) -> Result { + Self::from_base62(picture.get_uri()) + } +} + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct FileId(pub [u8; 20]); impl FileId { + pub fn from_raw(src: &[u8]) -> FileId { + let mut dst = [0u8; 20]; + dst.clone_from_slice(src); + FileId(dst) + } + pub fn to_base16(&self) -> String { to_base16(&self.0, &mut [0u8; 40]) } @@ -237,6 +483,29 @@ impl fmt::Display for FileId { } } +impl From<&[u8]> for FileId { + fn from(src: &[u8]) -> Self { + Self::from_raw(src) + } +} +impl From<&protocol::metadata::Image> for FileId { + fn from(image: &protocol::metadata::Image) -> Self { + Self::from(image.get_file_id()) + } +} + +impl From<&protocol::metadata::AudioFile> for FileId { + fn from(file: &protocol::metadata::AudioFile) -> Self { + Self::from(file.get_file_id()) + } +} + +impl From<&protocol::metadata::VideoFile> for FileId { + fn from(video: &protocol::metadata::VideoFile) -> Self { + Self::from(video.get_file_id()) + } +} + #[inline] fn to_base16(src: &[u8], buf: &mut [u8]) -> String { let mut i = 0; @@ -258,7 +527,8 @@ mod tests { struct ConversionCase { id: u128, - kind: SpotifyAudioType, + kind: SpotifyItemType, + uri_error: Option, uri: &'static str, base16: &'static str, base62: &'static str, @@ -268,7 +538,8 @@ mod tests { static CONV_VALID: [ConversionCase; 4] = [ ConversionCase { id: 238762092608182713602505436543891614649, - kind: SpotifyAudioType::Track, + kind: SpotifyItemType::Track, + uri_error: None, uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH", base16: "b39fe8081e1f4c54be38e8d6f9f12bb9", base62: "5sWHDYs0csV6RS48xBl0tH", @@ -278,7 +549,8 @@ mod tests { }, ConversionCase { id: 204841891221366092811751085145916697048, - kind: SpotifyAudioType::Track, + kind: SpotifyItemType::Track, + uri_error: None, uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", @@ -288,7 +560,8 @@ mod tests { }, ConversionCase { id: 204841891221366092811751085145916697048, - kind: SpotifyAudioType::Podcast, + kind: SpotifyItemType::Episode, + uri_error: None, uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", @@ -298,8 +571,9 @@ mod tests { }, ConversionCase { id: 204841891221366092811751085145916697048, - kind: SpotifyAudioType::NonPlayable, - uri: "spotify:unknown:4GNcXTGWmnZ3ySrqvol3o4", + kind: SpotifyItemType::Show, + uri_error: None, + uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", raw: &[ @@ -311,8 +585,9 @@ mod tests { static CONV_INVALID: [ConversionCase; 3] = [ ConversionCase { id: 0, - kind: SpotifyAudioType::NonPlayable, + kind: SpotifyItemType::Unknown, // Invalid ID in the URI. + uri_error: Some(SpotifyIdError::InvalidId), uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH", base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9", base62: "!!!!!Ys0csV6RS48xBl0tH", @@ -323,8 +598,9 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyAudioType::NonPlayable, + kind: SpotifyItemType::Unknown, // Missing colon between ID and type. + uri_error: Some(SpotifyIdError::InvalidFormat), uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", base16: "--------------------", base62: "....................", @@ -335,8 +611,9 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyAudioType::NonPlayable, + kind: SpotifyItemType::Unknown, // Uri too short + uri_error: Some(SpotifyIdError::InvalidId), uri: "spotify:azb:aRS48xBl0tH", base16: "--------------------", base62: "....................", @@ -354,7 +631,10 @@ mod tests { } for c in &CONV_INVALID { - assert_eq!(SpotifyId::from_base62(c.base62), Err(SpotifyIdError)); + assert_eq!( + SpotifyId::from_base62(c.base62), + Err(SpotifyIdError::InvalidId) + ); } } @@ -363,7 +643,7 @@ mod tests { for c in &CONV_VALID { let id = SpotifyId { id: c.id, - audio_type: c.kind, + item_type: c.kind, }; assert_eq!(id.to_base62(), c.base62); @@ -377,7 +657,10 @@ mod tests { } for c in &CONV_INVALID { - assert_eq!(SpotifyId::from_base16(c.base16), Err(SpotifyIdError)); + assert_eq!( + SpotifyId::from_base16(c.base16), + Err(SpotifyIdError::InvalidId) + ); } } @@ -386,7 +669,7 @@ mod tests { for c in &CONV_VALID { let id = SpotifyId { id: c.id, - audio_type: c.kind, + item_type: c.kind, }; assert_eq!(id.to_base16(), c.base16); @@ -399,11 +682,11 @@ mod tests { let actual = SpotifyId::from_uri(c.uri).unwrap(); assert_eq!(actual.id, c.id); - assert_eq!(actual.audio_type, c.kind); + assert_eq!(actual.item_type, c.kind); } for c in &CONV_INVALID { - assert_eq!(SpotifyId::from_uri(c.uri), Err(SpotifyIdError)); + assert_eq!(SpotifyId::from_uri(c.uri), Err(c.uri_error.unwrap())); } } @@ -412,7 +695,7 @@ mod tests { for c in &CONV_VALID { let id = SpotifyId { id: c.id, - audio_type: c.kind, + item_type: c.kind, }; assert_eq!(id.to_uri(), c.uri); @@ -426,7 +709,7 @@ mod tests { } for c in &CONV_INVALID { - assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError)); + assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError::InvalidId)); } } } diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 9409bae6..a12e12f8 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -11,9 +11,11 @@ edition = "2018" async-trait = "0.1" byteorder = "1.3" bytes = "1.0" +chrono = "0.4" log = "0.4" protobuf = "2.14.0" thiserror = "1" +uuid = { version = "0.8", default-features = false } [dependencies.librespot-core] path = "../core" diff --git a/metadata/src/album.rs b/metadata/src/album.rs new file mode 100644 index 00000000..fe01ee2b --- /dev/null +++ b/metadata/src/album.rs @@ -0,0 +1,151 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; +use std::ops::Deref; + +use crate::{ + artist::Artists, + availability::Availabilities, + copyright::Copyrights, + date::Date, + error::{MetadataError, RequestError}, + external_id::ExternalIds, + image::Images, + request::RequestResult, + restriction::Restrictions, + sale_period::SalePeriods, + track::Tracks, + util::try_from_repeated_message, + Metadata, +}; + +use librespot_core::session::Session; +use librespot_core::spotify_id::SpotifyId; +use librespot_protocol as protocol; + +use protocol::metadata::Disc as DiscMessage; + +pub use protocol::metadata::Album_Type as AlbumType; + +#[derive(Debug, Clone)] +pub struct Album { + pub id: SpotifyId, + pub name: String, + pub artists: Artists, + pub album_type: AlbumType, + pub label: String, + pub date: Date, + pub popularity: i32, + pub genres: Vec, + pub covers: Images, + pub external_ids: ExternalIds, + pub discs: Discs, + pub reviews: Vec, + pub copyrights: Copyrights, + pub restrictions: Restrictions, + pub related: Albums, + pub sale_periods: SalePeriods, + pub cover_group: Images, + pub original_title: String, + pub version_title: String, + pub type_str: String, + pub availability: Availabilities, +} + +#[derive(Debug, Clone)] +pub struct Albums(pub Vec); + +impl Deref for Albums { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct Disc { + pub number: i32, + pub name: String, + pub tracks: Tracks, +} + +#[derive(Debug, Clone)] +pub struct Discs(pub Vec); + +impl Deref for Discs { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Album { + pub fn tracks(&self) -> Tracks { + let result = self + .discs + .iter() + .flat_map(|disc| disc.tracks.deref().clone()) + .collect(); + Tracks(result) + } +} + +#[async_trait] +impl Metadata for Album { + type Message = protocol::metadata::Album; + + async fn request(session: &Session, album_id: SpotifyId) -> RequestResult { + session + .spclient() + .get_album_metadata(album_id) + .await + .map_err(RequestError::Http) + } + + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + Self::try_from(msg) + } +} + +impl TryFrom<&::Message> for Album { + type Error = MetadataError; + fn try_from(album: &::Message) -> Result { + Ok(Self { + id: album.try_into()?, + name: album.get_name().to_owned(), + artists: album.get_artist().try_into()?, + album_type: album.get_field_type(), + label: album.get_label().to_owned(), + date: album.get_date().into(), + popularity: album.get_popularity(), + genres: album.get_genre().to_vec(), + covers: album.get_cover().into(), + external_ids: album.get_external_id().into(), + discs: album.get_disc().try_into()?, + reviews: album.get_review().to_vec(), + copyrights: album.get_copyright().into(), + restrictions: album.get_restriction().into(), + related: album.get_related().try_into()?, + sale_periods: album.get_sale_period().into(), + cover_group: album.get_cover_group().get_image().into(), + original_title: album.get_original_title().to_owned(), + version_title: album.get_version_title().to_owned(), + type_str: album.get_type_str().to_owned(), + availability: album.get_availability().into(), + }) + } +} + +try_from_repeated_message!(::Message, Albums); + +impl TryFrom<&DiscMessage> for Disc { + type Error = MetadataError; + fn try_from(disc: &DiscMessage) -> Result { + Ok(Self { + number: disc.get_number(), + name: disc.get_name().to_owned(), + tracks: disc.get_track().try_into()?, + }) + } +} + +try_from_repeated_message!(DiscMessage, Discs); diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs new file mode 100644 index 00000000..517977bf --- /dev/null +++ b/metadata/src/artist.rs @@ -0,0 +1,139 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; +use std::ops::Deref; + +use crate::{ + error::{MetadataError, RequestError}, + request::RequestResult, + track::Tracks, + util::try_from_repeated_message, + Metadata, +}; + +use librespot_core::session::Session; +use librespot_core::spotify_id::SpotifyId; +use librespot_protocol as protocol; + +use protocol::metadata::ArtistWithRole as ArtistWithRoleMessage; +use protocol::metadata::TopTracks as TopTracksMessage; + +pub use protocol::metadata::ArtistWithRole_ArtistRole as ArtistRole; + +#[derive(Debug, Clone)] +pub struct Artist { + pub id: SpotifyId, + pub name: String, + pub top_tracks: CountryTopTracks, +} + +#[derive(Debug, Clone)] +pub struct Artists(pub Vec); + +impl Deref for Artists { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct ArtistWithRole { + pub id: SpotifyId, + pub name: String, + pub role: ArtistRole, +} + +#[derive(Debug, Clone)] +pub struct ArtistsWithRole(pub Vec); + +impl Deref for ArtistsWithRole { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct TopTracks { + pub country: String, + pub tracks: Tracks, +} + +#[derive(Debug, Clone)] +pub struct CountryTopTracks(pub Vec); + +impl Deref for CountryTopTracks { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl CountryTopTracks { + pub fn for_country(&self, country: &str) -> Tracks { + if let Some(country) = self.0.iter().find(|top_track| top_track.country == country) { + return country.tracks.clone(); + } + + if let Some(global) = self.0.iter().find(|top_track| top_track.country.is_empty()) { + return global.tracks.clone(); + } + + Tracks(vec![]) // none found + } +} + +#[async_trait] +impl Metadata for Artist { + type Message = protocol::metadata::Artist; + + async fn request(session: &Session, artist_id: SpotifyId) -> RequestResult { + session + .spclient() + .get_artist_metadata(artist_id) + .await + .map_err(RequestError::Http) + } + + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + Self::try_from(msg) + } +} + +impl TryFrom<&::Message> for Artist { + type Error = MetadataError; + fn try_from(artist: &::Message) -> Result { + Ok(Self { + id: artist.try_into()?, + name: artist.get_name().to_owned(), + top_tracks: artist.get_top_track().try_into()?, + }) + } +} + +try_from_repeated_message!(::Message, Artists); + +impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole { + type Error = MetadataError; + fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result { + Ok(Self { + id: artist_with_role.try_into()?, + name: artist_with_role.get_artist_name().to_owned(), + role: artist_with_role.get_role(), + }) + } +} + +try_from_repeated_message!(ArtistWithRoleMessage, ArtistsWithRole); + +impl TryFrom<&TopTracksMessage> for TopTracks { + type Error = MetadataError; + fn try_from(top_tracks: &TopTracksMessage) -> Result { + Ok(Self { + country: top_tracks.get_country().to_owned(), + tracks: top_tracks.get_track().try_into()?, + }) + } +} + +try_from_repeated_message!(TopTracksMessage, CountryTopTracks); diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs new file mode 100644 index 00000000..01ec984e --- /dev/null +++ b/metadata/src/audio/file.rs @@ -0,0 +1,31 @@ +use std::collections::HashMap; +use std::fmt::Debug; +use std::ops::Deref; + +use librespot_core::spotify_id::FileId; +use librespot_protocol as protocol; + +use protocol::metadata::AudioFile as AudioFileMessage; + +pub use protocol::metadata::AudioFile_Format as AudioFileFormat; + +#[derive(Debug, Clone)] +pub struct AudioFiles(pub HashMap); + +impl Deref for AudioFiles { + type Target = HashMap; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&[AudioFileMessage]> for AudioFiles { + fn from(files: &[AudioFileMessage]) -> Self { + let audio_files = files + .iter() + .map(|file| (file.get_format(), FileId::from(file.get_file_id()))) + .collect(); + + AudioFiles(audio_files) + } +} diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs new file mode 100644 index 00000000..09b72ebc --- /dev/null +++ b/metadata/src/audio/item.rs @@ -0,0 +1,104 @@ +use std::fmt::Debug; + +use chrono::Local; + +use crate::{ + availability::{AudioItemAvailability, Availabilities, UnavailabilityReason}, + episode::Episode, + error::MetadataError, + restriction::Restrictions, + track::{Track, Tracks}, +}; + +use super::file::AudioFiles; + +use librespot_core::session::Session; +use librespot_core::spotify_id::{SpotifyId, SpotifyItemType}; + +pub type AudioItemResult = Result; + +// A wrapper with fields the player needs +#[derive(Debug, Clone)] +pub struct AudioItem { + pub id: SpotifyId, + pub spotify_uri: String, + pub files: AudioFiles, + pub name: String, + pub duration: i32, + pub availability: AudioItemAvailability, + pub alternatives: Option, +} + +impl AudioItem { + pub async fn get_file(session: &Session, id: SpotifyId) -> AudioItemResult { + match id.item_type { + SpotifyItemType::Track => Track::get_audio_item(session, id).await, + SpotifyItemType::Episode => Episode::get_audio_item(session, id).await, + _ => Err(MetadataError::NonPlayable), + } + } +} + +#[async_trait] +pub trait InnerAudioItem { + async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult; + + fn allowed_in_country(restrictions: &Restrictions, country: &str) -> AudioItemAvailability { + for premium_restriction in restrictions.iter().filter(|restriction| { + restriction + .catalogue_strs + .iter() + .any(|catalogue| *catalogue == "premium") + }) { + if let Some(allowed_countries) = &premium_restriction.countries_allowed { + // A restriction will specify either a whitelast *or* a blacklist, + // but not both. So restrict availability if there is a whitelist + // and the country isn't on it. + if allowed_countries.iter().any(|allowed| country == *allowed) { + return Ok(()); + } else { + return Err(UnavailabilityReason::NotWhitelisted); + } + } + + if let Some(forbidden_countries) = &premium_restriction.countries_forbidden { + if forbidden_countries + .iter() + .any(|forbidden| country == *forbidden) + { + return Err(UnavailabilityReason::Blacklisted); + } else { + return Ok(()); + } + } + } + + Ok(()) // no restrictions in place + } + + fn available(availability: &Availabilities) -> AudioItemAvailability { + if availability.is_empty() { + // not all items have availability specified + return Ok(()); + } + + if !(availability + .iter() + .any(|availability| Local::now() >= availability.start.as_utc())) + { + return Err(UnavailabilityReason::Embargo); + } + + Ok(()) + } + + fn available_in_country( + availability: &Availabilities, + restrictions: &Restrictions, + country: &str, + ) -> AudioItemAvailability { + Self::available(availability)?; + Self::allowed_in_country(restrictions, country)?; + Ok(()) + } +} diff --git a/metadata/src/audio/mod.rs b/metadata/src/audio/mod.rs new file mode 100644 index 00000000..cc4efef0 --- /dev/null +++ b/metadata/src/audio/mod.rs @@ -0,0 +1,5 @@ +pub mod file; +pub mod item; + +pub use file::AudioFileFormat; +pub use item::AudioItem; diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs new file mode 100644 index 00000000..c40427cb --- /dev/null +++ b/metadata/src/availability.rs @@ -0,0 +1,49 @@ +use std::fmt::Debug; +use std::ops::Deref; + +use thiserror::Error; + +use crate::{date::Date, util::from_repeated_message}; + +use librespot_protocol as protocol; + +use protocol::metadata::Availability as AvailabilityMessage; + +pub type AudioItemAvailability = Result<(), UnavailabilityReason>; + +#[derive(Debug, Clone)] +pub struct Availability { + pub catalogue_strs: Vec, + pub start: Date, +} + +#[derive(Debug, Clone)] +pub struct Availabilities(pub Vec); + +impl Deref for Availabilities { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Copy, Clone, Error)] +pub enum UnavailabilityReason { + #[error("blacklist present and country on it")] + Blacklisted, + #[error("available date is in the future")] + Embargo, + #[error("whitelist present and country not on it")] + NotWhitelisted, +} + +impl From<&AvailabilityMessage> for Availability { + fn from(availability: &AvailabilityMessage) -> Self { + Self { + catalogue_strs: availability.get_catalogue_str().to_vec(), + start: availability.get_start().into(), + } + } +} + +from_repeated_message!(AvailabilityMessage, Availabilities); diff --git a/metadata/src/content_rating.rs b/metadata/src/content_rating.rs new file mode 100644 index 00000000..a6f061d0 --- /dev/null +++ b/metadata/src/content_rating.rs @@ -0,0 +1,35 @@ +use std::fmt::Debug; +use std::ops::Deref; + +use crate::util::from_repeated_message; + +use librespot_protocol as protocol; + +use protocol::metadata::ContentRating as ContentRatingMessage; + +#[derive(Debug, Clone)] +pub struct ContentRating { + pub country: String, + pub tags: Vec, +} + +#[derive(Debug, Clone)] +pub struct ContentRatings(pub Vec); + +impl Deref for ContentRatings { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&ContentRatingMessage> for ContentRating { + fn from(content_rating: &ContentRatingMessage) -> Self { + Self { + country: content_rating.get_country().to_owned(), + tags: content_rating.get_tag().to_vec(), + } + } +} + +from_repeated_message!(ContentRatingMessage, ContentRatings); diff --git a/metadata/src/copyright.rs b/metadata/src/copyright.rs new file mode 100644 index 00000000..7842b7dd --- /dev/null +++ b/metadata/src/copyright.rs @@ -0,0 +1,37 @@ +use std::fmt::Debug; +use std::ops::Deref; + +use librespot_protocol as protocol; + +use crate::util::from_repeated_message; + +use protocol::metadata::Copyright as CopyrightMessage; + +pub use protocol::metadata::Copyright_Type as CopyrightType; + +#[derive(Debug, Clone)] +pub struct Copyright { + pub copyright_type: CopyrightType, + pub text: String, +} + +#[derive(Debug, Clone)] +pub struct Copyrights(pub Vec); + +impl Deref for Copyrights { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&CopyrightMessage> for Copyright { + fn from(copyright: &CopyrightMessage) -> Self { + Self { + copyright_type: copyright.get_field_type(), + text: copyright.get_text().to_owned(), + } + } +} + +from_repeated_message!(CopyrightMessage, Copyrights); diff --git a/metadata/src/cover.rs b/metadata/src/cover.rs deleted file mode 100644 index b483f454..00000000 --- a/metadata/src/cover.rs +++ /dev/null @@ -1,20 +0,0 @@ -use byteorder::{BigEndian, WriteBytesExt}; -use std::io::Write; - -use librespot_core::channel::ChannelData; -use librespot_core::packet::PacketType; -use librespot_core::session::Session; -use librespot_core::spotify_id::FileId; - -pub fn get(session: &Session, file: FileId) -> ChannelData { - let (channel_id, channel) = session.channel().allocate(); - let (_headers, data) = channel.split(); - - let mut packet: Vec = Vec::new(); - packet.write_u16::(channel_id).unwrap(); - packet.write_u16::(0).unwrap(); - packet.write(&file.0).unwrap(); - session.send_packet(PacketType::Image, packet); - - data -} diff --git a/metadata/src/date.rs b/metadata/src/date.rs new file mode 100644 index 00000000..c402c05f --- /dev/null +++ b/metadata/src/date.rs @@ -0,0 +1,70 @@ +use std::convert::TryFrom; +use std::fmt::Debug; +use std::ops::Deref; + +use chrono::{DateTime, Utc}; +use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; + +use crate::error::MetadataError; + +use librespot_protocol as protocol; + +use protocol::metadata::Date as DateMessage; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Date(pub DateTime); + +impl Deref for Date { + type Target = DateTime; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Date { + pub fn as_timestamp(&self) -> i64 { + self.0.timestamp() + } + + pub fn from_timestamp(timestamp: i64) -> Result { + if let Some(date_time) = NaiveDateTime::from_timestamp_opt(timestamp, 0) { + Ok(Self::from_utc(date_time)) + } else { + Err(MetadataError::InvalidTimestamp) + } + } + + pub fn as_utc(&self) -> DateTime { + self.0 + } + + pub fn from_utc(date_time: NaiveDateTime) -> Self { + Self(DateTime::::from_utc(date_time, Utc)) + } +} + +impl From<&DateMessage> for Date { + fn from(date: &DateMessage) -> Self { + let naive_date = NaiveDate::from_ymd( + date.get_year() as i32, + date.get_month() as u32, + date.get_day() as u32, + ); + let naive_time = NaiveTime::from_hms(date.get_hour() as u32, date.get_minute() as u32, 0); + let naive_datetime = NaiveDateTime::new(naive_date, naive_time); + Self(DateTime::::from_utc(naive_datetime, Utc)) + } +} + +impl From> for Date { + fn from(date: DateTime) -> Self { + Self(date) + } +} + +impl TryFrom for Date { + type Error = MetadataError; + fn try_from(timestamp: i64) -> Result { + Self::from_timestamp(timestamp) + } +} diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs new file mode 100644 index 00000000..35d6ed8f --- /dev/null +++ b/metadata/src/episode.rs @@ -0,0 +1,132 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; +use std::ops::Deref; + +use crate::{ + audio::{ + file::AudioFiles, + item::{AudioItem, AudioItemResult, InnerAudioItem}, + }, + availability::Availabilities, + date::Date, + error::{MetadataError, RequestError}, + image::Images, + request::RequestResult, + restriction::Restrictions, + util::try_from_repeated_message, + video::VideoFiles, + Metadata, +}; + +use librespot_core::session::Session; +use librespot_core::spotify_id::SpotifyId; +use librespot_protocol as protocol; + +pub use protocol::metadata::Episode_EpisodeType as EpisodeType; + +#[derive(Debug, Clone)] +pub struct Episode { + pub id: SpotifyId, + pub name: String, + pub duration: i32, + pub audio: AudioFiles, + pub description: String, + pub number: i32, + pub publish_time: Date, + pub covers: Images, + pub language: String, + pub is_explicit: bool, + pub show: SpotifyId, + pub videos: VideoFiles, + pub video_previews: VideoFiles, + pub audio_previews: AudioFiles, + pub restrictions: Restrictions, + pub freeze_frames: Images, + pub keywords: Vec, + pub allow_background_playback: bool, + pub availability: Availabilities, + pub external_url: String, + pub episode_type: EpisodeType, + pub has_music_and_talk: bool, +} + +#[derive(Debug, Clone)] +pub struct Episodes(pub Vec); + +impl Deref for Episodes { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[async_trait] +impl InnerAudioItem for Episode { + async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult { + let episode = Self::get(session, id).await?; + let availability = Self::available_in_country( + &episode.availability, + &episode.restrictions, + &session.country(), + ); + + Ok(AudioItem { + id, + spotify_uri: id.to_uri(), + files: episode.audio, + name: episode.name, + duration: episode.duration, + availability, + alternatives: None, + }) + } +} + +#[async_trait] +impl Metadata for Episode { + type Message = protocol::metadata::Episode; + + async fn request(session: &Session, episode_id: SpotifyId) -> RequestResult { + session + .spclient() + .get_episode_metadata(episode_id) + .await + .map_err(RequestError::Http) + } + + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + Self::try_from(msg) + } +} + +impl TryFrom<&::Message> for Episode { + type Error = MetadataError; + fn try_from(episode: &::Message) -> Result { + Ok(Self { + id: episode.try_into()?, + name: episode.get_name().to_owned(), + duration: episode.get_duration().to_owned(), + audio: episode.get_audio().into(), + description: episode.get_description().to_owned(), + number: episode.get_number(), + publish_time: episode.get_publish_time().into(), + covers: episode.get_cover_image().get_image().into(), + language: episode.get_language().to_owned(), + is_explicit: episode.get_explicit().to_owned(), + show: episode.get_show().try_into()?, + videos: episode.get_video().into(), + video_previews: episode.get_video_preview().into(), + audio_previews: episode.get_audio_preview().into(), + restrictions: episode.get_restriction().into(), + freeze_frames: episode.get_freeze_frame().get_image().into(), + keywords: episode.get_keyword().to_vec(), + allow_background_playback: episode.get_allow_background_playback(), + availability: episode.get_availability().into(), + external_url: episode.get_external_url().to_owned(), + episode_type: episode.get_field_type(), + has_music_and_talk: episode.get_music_and_talk(), + }) + } +} + +try_from_repeated_message!(::Message, Episodes); diff --git a/metadata/src/error.rs b/metadata/src/error.rs new file mode 100644 index 00000000..2aeaef1e --- /dev/null +++ b/metadata/src/error.rs @@ -0,0 +1,34 @@ +use std::fmt::Debug; +use thiserror::Error; + +use protobuf::ProtobufError; + +use librespot_core::mercury::MercuryError; +use librespot_core::spclient::SpClientError; +use librespot_core::spotify_id::SpotifyIdError; + +#[derive(Debug, Error)] +pub enum RequestError { + #[error("could not get metadata over HTTP: {0}")] + Http(#[from] SpClientError), + #[error("could not get metadata over Mercury: {0}")] + Mercury(#[from] MercuryError), + #[error("response was empty")] + Empty, +} + +#[derive(Debug, Error)] +pub enum MetadataError { + #[error("{0}")] + InvalidSpotifyId(#[from] SpotifyIdError), + #[error("item has invalid date")] + InvalidTimestamp, + #[error("audio item is non-playable")] + NonPlayable, + #[error("could not parse protobuf: {0}")] + Protobuf(#[from] ProtobufError), + #[error("error executing request: {0}")] + Request(#[from] RequestError), + #[error("could not parse repeated fields")] + InvalidRepeated, +} diff --git a/metadata/src/external_id.rs b/metadata/src/external_id.rs new file mode 100644 index 00000000..31755e72 --- /dev/null +++ b/metadata/src/external_id.rs @@ -0,0 +1,35 @@ +use std::fmt::Debug; +use std::ops::Deref; + +use crate::util::from_repeated_message; + +use librespot_protocol as protocol; + +use protocol::metadata::ExternalId as ExternalIdMessage; + +#[derive(Debug, Clone)] +pub struct ExternalId { + pub external_type: String, + pub id: String, +} + +#[derive(Debug, Clone)] +pub struct ExternalIds(pub Vec); + +impl Deref for ExternalIds { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&ExternalIdMessage> for ExternalId { + fn from(external_id: &ExternalIdMessage) -> Self { + Self { + external_type: external_id.get_field_type().to_owned(), + id: external_id.get_id().to_owned(), + } + } +} + +from_repeated_message!(ExternalIdMessage, ExternalIds); diff --git a/metadata/src/image.rs b/metadata/src/image.rs new file mode 100644 index 00000000..b6653d09 --- /dev/null +++ b/metadata/src/image.rs @@ -0,0 +1,103 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; +use std::ops::Deref; + +use crate::{ + error::MetadataError, + util::{from_repeated_message, try_from_repeated_message}, +}; + +use librespot_core::spotify_id::{FileId, SpotifyId}; +use librespot_protocol as protocol; + +use protocol::metadata::Image as ImageMessage; +use protocol::playlist4_external::PictureSize as PictureSizeMessage; +use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage; + +pub use protocol::metadata::Image_Size as ImageSize; + +#[derive(Debug, Clone)] +pub struct Image { + pub id: FileId, + pub size: ImageSize, + pub width: i32, + pub height: i32, +} + +#[derive(Debug, Clone)] +pub struct Images(pub Vec); + +impl Deref for Images { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct PictureSize { + pub target_name: String, + pub url: String, +} + +#[derive(Debug, Clone)] +pub struct PictureSizes(pub Vec); + +impl Deref for PictureSizes { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct TranscodedPicture { + pub target_name: String, + pub uri: SpotifyId, +} + +#[derive(Debug, Clone)] +pub struct TranscodedPictures(pub Vec); + +impl Deref for TranscodedPictures { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&ImageMessage> for Image { + fn from(image: &ImageMessage) -> Self { + Self { + id: image.into(), + size: image.get_size(), + width: image.get_width(), + height: image.get_height(), + } + } +} + +from_repeated_message!(ImageMessage, Images); + +impl From<&PictureSizeMessage> for PictureSize { + fn from(size: &PictureSizeMessage) -> Self { + Self { + target_name: size.get_target_name().to_owned(), + url: size.get_url().to_owned(), + } + } +} + +from_repeated_message!(PictureSizeMessage, PictureSizes); + +impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture { + type Error = MetadataError; + fn try_from(picture: &TranscodedPictureMessage) -> Result { + Ok(Self { + target_name: picture.get_target_name().to_owned(), + uri: picture.try_into()?, + }) + } +} + +try_from_repeated_message!(TranscodedPictureMessage, TranscodedPictures); diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 05ab028d..f1090b0f 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -1,643 +1,51 @@ -#![allow(clippy::unused_io_amount)] - #[macro_use] extern crate log; #[macro_use] extern crate async_trait; -pub mod cover; +use protobuf::Message; -use std::collections::HashMap; - -use librespot_core::mercury::MercuryError; use librespot_core::session::Session; -use librespot_core::spclient::SpClientError; -use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId}; -use librespot_protocol as protocol; -use protobuf::{Message, ProtobufError}; +use librespot_core::spotify_id::SpotifyId; -use thiserror::Error; +pub mod album; +pub mod artist; +pub mod audio; +pub mod availability; +pub mod content_rating; +pub mod copyright; +pub mod date; +pub mod episode; +pub mod error; +pub mod external_id; +pub mod image; +pub mod playlist; +mod request; +pub mod restriction; +pub mod sale_period; +pub mod show; +pub mod track; +mod util; +pub mod video; -pub use crate::protocol::metadata::AudioFile_Format as FileFormat; - -fn countrylist_contains(list: &str, country: &str) -> bool { - list.chunks(2).any(|cc| cc == country) -} - -fn parse_restrictions<'s, I>(restrictions: I, country: &str, catalogue: &str) -> bool -where - I: IntoIterator, -{ - let mut forbidden = "".to_string(); - let mut has_forbidden = false; - - let mut allowed = "".to_string(); - let mut has_allowed = false; - - let rs = restrictions - .into_iter() - .filter(|r| r.get_catalogue_str().contains(&catalogue.to_owned())); - - for r in rs { - if r.has_countries_forbidden() { - forbidden.push_str(r.get_countries_forbidden()); - has_forbidden = true; - } - - if r.has_countries_allowed() { - allowed.push_str(r.get_countries_allowed()); - has_allowed = true; - } - } - - !(has_forbidden && countrylist_contains(forbidden.as_str(), country) - || has_allowed && !countrylist_contains(allowed.as_str(), country)) -} - -// A wrapper with fields the player needs -#[derive(Debug, Clone)] -pub struct AudioItem { - pub id: SpotifyId, - pub uri: String, - pub files: HashMap, - pub name: String, - pub duration: i32, - pub available: bool, - pub alternatives: Option>, -} - -impl AudioItem { - pub async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { - match id.audio_type { - SpotifyAudioType::Track => Track::get_audio_item(session, id).await, - SpotifyAudioType::Podcast => Episode::get_audio_item(session, id).await, - SpotifyAudioType::NonPlayable => Err(MetadataError::NonPlayable), - } - } -} - -pub type AudioItemResult = Result; - -#[async_trait] -trait AudioFiles { - async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult; -} - -#[async_trait] -impl AudioFiles for Track { - async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult { - let item = Self::get(session, id).await?; - let alternatives = { - if item.alternatives.is_empty() { - None - } else { - Some(item.alternatives) - } - }; - - Ok(AudioItem { - id, - uri: format!("spotify:track:{}", id.to_base62()), - files: item.files, - name: item.name, - duration: item.duration, - available: item.available, - alternatives, - }) - } -} - -#[async_trait] -impl AudioFiles for Episode { - async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult { - let item = Self::get(session, id).await?; - - Ok(AudioItem { - id, - uri: format!("spotify:episode:{}", id.to_base62()), - files: item.files, - name: item.name, - duration: item.duration, - available: item.available, - alternatives: None, - }) - } -} - -#[derive(Debug, Error)] -pub enum MetadataError { - #[error("could not get metadata over HTTP: {0}")] - Http(#[from] SpClientError), - #[error("could not get metadata over Mercury: {0}")] - Mercury(#[from] MercuryError), - #[error("could not parse metadata: {0}")] - Parsing(#[from] ProtobufError), - #[error("response was empty")] - Empty, - #[error("audio item is non-playable")] - NonPlayable, -} - -pub type MetadataResult = Result; +use error::MetadataError; +use request::RequestResult; #[async_trait] pub trait Metadata: Send + Sized + 'static { type Message: protobuf::Message; - async fn request(session: &Session, id: SpotifyId) -> MetadataResult; - fn parse(msg: &Self::Message, session: &Session) -> Self; + // Request a protobuf + async fn request(session: &Session, id: SpotifyId) -> RequestResult; + // Request a metadata struct async fn get(session: &Session, id: SpotifyId) -> Result { let response = Self::request(session, id).await?; let msg = Self::Message::parse_from_bytes(&response)?; - Ok(Self::parse(&msg, session)) - } -} - -// TODO: expose more fields available in the protobufs - -#[derive(Debug, Clone)] -pub struct Track { - pub id: SpotifyId, - pub name: String, - pub duration: i32, - pub album: SpotifyId, - pub artists: Vec, - pub files: HashMap, - pub alternatives: Vec, - pub available: bool, -} - -#[derive(Debug, Clone)] -pub struct Album { - pub id: SpotifyId, - pub name: String, - pub artists: Vec, - pub tracks: Vec, - pub covers: Vec, -} - -#[derive(Debug, Clone)] -pub struct Episode { - pub id: SpotifyId, - pub name: String, - pub external_url: String, - pub duration: i32, - pub language: String, - pub show: SpotifyId, - pub files: HashMap, - pub covers: Vec, - pub available: bool, - pub explicit: bool, -} - -#[derive(Debug, Clone)] -pub struct Show { - pub id: SpotifyId, - pub name: String, - pub publisher: String, - pub episodes: Vec, - pub covers: Vec, -} - -#[derive(Debug, Clone)] -pub struct TranscodedPicture { - pub target_name: String, - pub uri: String, -} - -#[derive(Debug, Clone)] -pub struct PlaylistAnnotation { - pub description: String, - pub picture: String, - pub transcoded_pictures: Vec, - pub abuse_reporting: bool, - pub taken_down: bool, -} - -#[derive(Debug, Clone)] -pub struct Playlist { - pub revision: Vec, - pub user: String, - pub name: String, - pub tracks: Vec, -} - -#[derive(Debug, Clone)] -pub struct Artist { - pub id: SpotifyId, - pub name: String, - pub top_tracks: Vec, -} - -#[async_trait] -impl Metadata for Track { - type Message = protocol::metadata::Track; - - async fn request(session: &Session, track_id: SpotifyId) -> MetadataResult { - session - .spclient() - .get_track_metadata(track_id) - .await - .map_err(MetadataError::Http) - } - - fn parse(msg: &Self::Message, session: &Session) -> Self { - debug!("MESSAGE: {:?}", msg); - let country = session.country(); - - let artists = msg - .get_artist() - .iter() - .filter(|artist| artist.has_gid()) - .map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap()) - .collect::>(); - - let files = msg - .get_file() - .iter() - .filter(|file| file.has_file_id()) - .map(|file| { - let mut dst = [0u8; 20]; - dst.clone_from_slice(file.get_file_id()); - (file.get_format(), FileId(dst)) - }) - .collect(); - - Self { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), - name: msg.get_name().to_owned(), - duration: msg.get_duration(), - album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(), - artists, - files, - alternatives: msg - .get_alternative() - .iter() - .map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap()) - .collect(), - available: parse_restrictions(msg.get_restriction(), &country, "premium"), - } - } -} - -#[async_trait] -impl Metadata for Album { - type Message = protocol::metadata::Album; - - async fn request(session: &Session, album_id: SpotifyId) -> MetadataResult { - session - .spclient() - .get_album_metadata(album_id) - .await - .map_err(MetadataError::Http) - } - - fn parse(msg: &Self::Message, _: &Session) -> Self { - let artists = msg - .get_artist() - .iter() - .filter(|artist| artist.has_gid()) - .map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap()) - .collect::>(); - - let tracks = msg - .get_disc() - .iter() - .flat_map(|disc| disc.get_track()) - .filter(|track| track.has_gid()) - .map(|track| SpotifyId::from_raw(track.get_gid()).unwrap()) - .collect::>(); - - let covers = msg - .get_cover_group() - .get_image() - .iter() - .filter(|image| image.has_file_id()) - .map(|image| { - let mut dst = [0u8; 20]; - dst.clone_from_slice(image.get_file_id()); - FileId(dst) - }) - .collect::>(); - - Self { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), - name: msg.get_name().to_owned(), - artists, - tracks, - covers, - } - } -} - -#[async_trait] -impl Metadata for PlaylistAnnotation { - type Message = protocol::playlist_annotate3::PlaylistAnnotation; - - async fn request(session: &Session, playlist_id: SpotifyId) -> MetadataResult { - let current_user = session.username(); - Self::request_for_user(session, current_user, playlist_id).await - } - - fn parse(msg: &Self::Message, _: &Session) -> Self { - let transcoded_pictures = msg - .get_transcoded_picture() - .iter() - .map(|picture| TranscodedPicture { - target_name: picture.get_target_name().to_string(), - uri: picture.get_uri().to_string(), - }) - .collect::>(); - - let taken_down = !matches!( - msg.get_abuse_report_state(), - protocol::playlist_annotate3::AbuseReportState::OK - ); - - Self { - description: msg.get_description().to_string(), - picture: msg.get_picture().to_string(), - transcoded_pictures, - abuse_reporting: msg.get_is_abuse_reporting_enabled(), - taken_down, - } - } -} - -impl PlaylistAnnotation { - async fn request_for_user( - session: &Session, - username: String, - playlist_id: SpotifyId, - ) -> MetadataResult { - let uri = format!( - "hm://playlist-annotate/v1/annotation/user/{}/playlist/{}", - username, - playlist_id.to_base62() - ); - let response = session.mercury().get(uri).await?; - match response.payload.first() { - Some(data) => Ok(data.to_vec().into()), - None => Err(MetadataError::Empty), - } - } - - #[allow(dead_code)] - async fn get_for_user( - session: &Session, - username: String, - playlist_id: SpotifyId, - ) -> Result { - let response = Self::request_for_user(session, username, playlist_id).await?; - let msg = ::Message::parse_from_bytes(&response)?; - Ok(Self::parse(&msg, session)) - } -} - -#[async_trait] -impl Metadata for Playlist { - type Message = protocol::playlist4_external::SelectedListContent; - - async fn request(session: &Session, playlist_id: SpotifyId) -> MetadataResult { - let uri = format!("hm://playlist/v2/playlist/{}", playlist_id.to_base62()); - let response = session.mercury().get(uri).await?; - match response.payload.first() { - Some(data) => Ok(data.to_vec().into()), - None => Err(MetadataError::Empty), - } - } - - fn parse(msg: &Self::Message, _: &Session) -> Self { - let tracks = msg - .get_contents() - .get_items() - .iter() - .map(|item| { - let uri_split = item.get_uri().split(':'); - let uri_parts: Vec<&str> = uri_split.collect(); - SpotifyId::from_base62(uri_parts[2]).unwrap() - }) - .collect::>(); - - if tracks.len() != msg.get_length() as usize { - warn!( - "Got {} tracks, but the playlist should contain {} tracks.", - tracks.len(), - msg.get_length() - ); - } - - Self { - revision: msg.get_revision().to_vec(), - name: msg.get_attributes().get_name().to_owned(), - tracks, - user: msg.get_owner_username().to_string(), - } - } -} - -impl Playlist { - async fn request_for_user( - session: &Session, - username: String, - playlist_id: SpotifyId, - ) -> MetadataResult { - let uri = format!( - "hm://playlist/user/{}/playlist/{}", - username, - playlist_id.to_base62() - ); - let response = session.mercury().get(uri).await?; - match response.payload.first() { - Some(data) => Ok(data.to_vec().into()), - None => Err(MetadataError::Empty), - } - } - - async fn request_root_for_user(session: &Session, username: String) -> MetadataResult { - let uri = format!("hm://playlist/user/{}/rootlist", username); - let response = session.mercury().get(uri).await?; - match response.payload.first() { - Some(data) => Ok(data.to_vec().into()), - None => Err(MetadataError::Empty), - } - } - #[allow(dead_code)] - async fn get_for_user( - session: &Session, - username: String, - playlist_id: SpotifyId, - ) -> Result { - let response = Self::request_for_user(session, username, playlist_id).await?; - let msg = ::Message::parse_from_bytes(&response)?; - Ok(Self::parse(&msg, session)) - } - - #[allow(dead_code)] - async fn get_root_for_user(session: &Session, username: String) -> Result { - let response = Self::request_root_for_user(session, username).await?; - let msg = ::Message::parse_from_bytes(&response)?; - Ok(Self::parse(&msg, session)) - } -} - -#[async_trait] -impl Metadata for Artist { - type Message = protocol::metadata::Artist; - - async fn request(session: &Session, artist_id: SpotifyId) -> MetadataResult { - session - .spclient() - .get_artist_metadata(artist_id) - .await - .map_err(MetadataError::Http) - } - - fn parse(msg: &Self::Message, session: &Session) -> Self { - let country = session.country(); - - let top_tracks: Vec = match msg - .get_top_track() - .iter() - .find(|tt| !tt.has_country() || countrylist_contains(tt.get_country(), &country)) - { - Some(tracks) => tracks - .get_track() - .iter() - .filter(|track| track.has_gid()) - .map(|track| SpotifyId::from_raw(track.get_gid()).unwrap()) - .collect::>(), - None => Vec::new(), - }; - - Self { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), - name: msg.get_name().to_owned(), - top_tracks, - } - } -} - -// Podcast -#[async_trait] -impl Metadata for Episode { - type Message = protocol::metadata::Episode; - - async fn request(session: &Session, episode_id: SpotifyId) -> MetadataResult { - session - .spclient() - .get_album_metadata(episode_id) - .await - .map_err(MetadataError::Http) - } - - fn parse(msg: &Self::Message, session: &Session) -> Self { - let country = session.country(); - - let files = msg - .get_audio() - .iter() - .filter(|file| file.has_file_id()) - .map(|file| { - let mut dst = [0u8; 20]; - dst.clone_from_slice(file.get_file_id()); - (file.get_format(), FileId(dst)) - }) - .collect(); - - let covers = msg - .get_cover_image() - .get_image() - .iter() - .filter(|image| image.has_file_id()) - .map(|image| { - let mut dst = [0u8; 20]; - dst.clone_from_slice(image.get_file_id()); - FileId(dst) - }) - .collect::>(); - - Self { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), - name: msg.get_name().to_owned(), - external_url: msg.get_external_url().to_owned(), - duration: msg.get_duration().to_owned(), - language: msg.get_language().to_owned(), - show: SpotifyId::from_raw(msg.get_show().get_gid()).unwrap(), - covers, - files, - available: parse_restrictions(msg.get_restriction(), &country, "premium"), - explicit: msg.get_explicit().to_owned(), - } - } -} - -#[async_trait] -impl Metadata for Show { - type Message = protocol::metadata::Show; - - async fn request(session: &Session, show_id: SpotifyId) -> MetadataResult { - session - .spclient() - .get_show_metadata(show_id) - .await - .map_err(MetadataError::Http) - } - - fn parse(msg: &Self::Message, _: &Session) -> Self { - let episodes = msg - .get_episode() - .iter() - .filter(|episode| episode.has_gid()) - .map(|episode| SpotifyId::from_raw(episode.get_gid()).unwrap()) - .collect::>(); - - let covers = msg - .get_cover_image() - .get_image() - .iter() - .filter(|image| image.has_file_id()) - .map(|image| { - let mut dst = [0u8; 20]; - dst.clone_from_slice(image.get_file_id()); - FileId(dst) - }) - .collect::>(); - - Self { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), - name: msg.get_name().to_owned(), - publisher: msg.get_publisher().to_owned(), - episodes, - covers, - } - } -} - -struct StrChunks<'s>(&'s str, usize); - -trait StrChunksExt { - fn chunks(&self, size: usize) -> StrChunks; -} - -impl StrChunksExt for str { - fn chunks(&self, size: usize) -> StrChunks { - StrChunks(self, size) - } -} - -impl<'s> Iterator for StrChunks<'s> { - type Item = &'s str; - fn next(&mut self) -> Option<&'s str> { - let &mut StrChunks(data, size) = self; - if data.is_empty() { - None - } else { - let ret = Some(&data[..size]); - self.0 = &data[size..]; - ret - } + trace!("Received metadata: {:?}", msg); + Self::parse(&msg, id) } + + fn parse(msg: &Self::Message, _: SpotifyId) -> Result; } diff --git a/metadata/src/playlist/annotation.rs b/metadata/src/playlist/annotation.rs new file mode 100644 index 00000000..0116d997 --- /dev/null +++ b/metadata/src/playlist/annotation.rs @@ -0,0 +1,89 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; + +use protobuf::Message; + +use crate::{ + error::MetadataError, + image::TranscodedPictures, + request::{MercuryRequest, RequestResult}, + Metadata, +}; + +use librespot_core::session::Session; +use librespot_core::spotify_id::SpotifyId; +use librespot_protocol as protocol; + +pub use protocol::playlist_annotate3::AbuseReportState; + +#[derive(Debug, Clone)] +pub struct PlaylistAnnotation { + pub description: String, + pub picture: String, + pub transcoded_pictures: TranscodedPictures, + pub has_abuse_reporting: bool, + pub abuse_report_state: AbuseReportState, +} + +#[async_trait] +impl Metadata for PlaylistAnnotation { + type Message = protocol::playlist_annotate3::PlaylistAnnotation; + + async fn request(session: &Session, playlist_id: SpotifyId) -> RequestResult { + let current_user = session.username(); + Self::request_for_user(session, ¤t_user, playlist_id).await + } + + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + Ok(Self { + description: msg.get_description().to_owned(), + picture: msg.get_picture().to_owned(), // TODO: is this a URL or Spotify URI? + transcoded_pictures: msg.get_transcoded_picture().try_into()?, + has_abuse_reporting: msg.get_is_abuse_reporting_enabled(), + abuse_report_state: msg.get_abuse_report_state(), + }) + } +} + +impl PlaylistAnnotation { + async fn request_for_user( + session: &Session, + username: &str, + playlist_id: SpotifyId, + ) -> RequestResult { + let uri = format!( + "hm://playlist-annotate/v1/annotation/user/{}/playlist/{}", + username, + playlist_id.to_base62() + ); + ::request(session, &uri).await + } + + #[allow(dead_code)] + async fn get_for_user( + session: &Session, + username: &str, + playlist_id: SpotifyId, + ) -> Result { + let response = Self::request_for_user(session, username, playlist_id).await?; + let msg = ::Message::parse_from_bytes(&response)?; + Self::parse(&msg, playlist_id) + } +} + +impl MercuryRequest for PlaylistAnnotation {} + +impl TryFrom<&::Message> for PlaylistAnnotation { + type Error = MetadataError; + fn try_from( + annotation: &::Message, + ) -> Result { + Ok(Self { + description: annotation.get_description().to_owned(), + picture: annotation.get_picture().to_owned(), + transcoded_pictures: annotation.get_transcoded_picture().try_into()?, + has_abuse_reporting: annotation.get_is_abuse_reporting_enabled(), + abuse_report_state: annotation.get_abuse_report_state(), + }) + } +} diff --git a/metadata/src/playlist/attribute.rs b/metadata/src/playlist/attribute.rs new file mode 100644 index 00000000..f00a2b13 --- /dev/null +++ b/metadata/src/playlist/attribute.rs @@ -0,0 +1,195 @@ +use std::collections::HashMap; +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; +use std::ops::Deref; + +use crate::{date::Date, error::MetadataError, image::PictureSizes, util::from_repeated_enum}; + +use librespot_core::spotify_id::SpotifyId; +use librespot_protocol as protocol; + +use protocol::playlist4_external::FormatListAttribute as PlaylistFormatAttributeMessage; +use protocol::playlist4_external::ItemAttributes as PlaylistItemAttributesMessage; +use protocol::playlist4_external::ItemAttributesPartialState as PlaylistPartialItemAttributesMessage; +use protocol::playlist4_external::ListAttributes as PlaylistAttributesMessage; +use protocol::playlist4_external::ListAttributesPartialState as PlaylistPartialAttributesMessage; +use protocol::playlist4_external::UpdateItemAttributes as PlaylistUpdateItemAttributesMessage; +use protocol::playlist4_external::UpdateListAttributes as PlaylistUpdateAttributesMessage; + +pub use protocol::playlist4_external::ItemAttributeKind as PlaylistItemAttributeKind; +pub use protocol::playlist4_external::ListAttributeKind as PlaylistAttributeKind; + +#[derive(Debug, Clone)] +pub struct PlaylistAttributes { + pub name: String, + pub description: String, + pub picture: SpotifyId, + pub is_collaborative: bool, + pub pl3_version: String, + pub is_deleted_by_owner: bool, + pub client_id: String, + pub format: String, + pub format_attributes: PlaylistFormatAttribute, + pub picture_sizes: PictureSizes, +} + +#[derive(Debug, Clone)] +pub struct PlaylistAttributeKinds(pub Vec); + +impl Deref for PlaylistAttributeKinds { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +from_repeated_enum!(PlaylistAttributeKind, PlaylistAttributeKinds); + +#[derive(Debug, Clone)] +pub struct PlaylistFormatAttribute(pub HashMap); + +impl Deref for PlaylistFormatAttribute { + type Target = HashMap; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct PlaylistItemAttributes { + pub added_by: String, + pub timestamp: Date, + pub seen_at: Date, + pub is_public: bool, + pub format_attributes: PlaylistFormatAttribute, + pub item_id: SpotifyId, +} + +#[derive(Debug, Clone)] +pub struct PlaylistItemAttributeKinds(pub Vec); + +impl Deref for PlaylistItemAttributeKinds { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +from_repeated_enum!(PlaylistItemAttributeKind, PlaylistItemAttributeKinds); + +#[derive(Debug, Clone)] +pub struct PlaylistPartialAttributes { + #[allow(dead_code)] + values: PlaylistAttributes, + #[allow(dead_code)] + no_value: PlaylistAttributeKinds, +} + +#[derive(Debug, Clone)] +pub struct PlaylistPartialItemAttributes { + #[allow(dead_code)] + values: PlaylistItemAttributes, + #[allow(dead_code)] + no_value: PlaylistItemAttributeKinds, +} + +#[derive(Debug, Clone)] +pub struct PlaylistUpdateAttributes { + pub new_attributes: PlaylistPartialAttributes, + pub old_attributes: PlaylistPartialAttributes, +} + +#[derive(Debug, Clone)] +pub struct PlaylistUpdateItemAttributes { + pub index: i32, + pub new_attributes: PlaylistPartialItemAttributes, + pub old_attributes: PlaylistPartialItemAttributes, +} + +impl TryFrom<&PlaylistAttributesMessage> for PlaylistAttributes { + type Error = MetadataError; + fn try_from(attributes: &PlaylistAttributesMessage) -> Result { + Ok(Self { + name: attributes.get_name().to_owned(), + description: attributes.get_description().to_owned(), + picture: attributes.get_picture().try_into()?, + is_collaborative: attributes.get_collaborative(), + pl3_version: attributes.get_pl3_version().to_owned(), + is_deleted_by_owner: attributes.get_deleted_by_owner(), + client_id: attributes.get_client_id().to_owned(), + format: attributes.get_format().to_owned(), + format_attributes: attributes.get_format_attributes().into(), + picture_sizes: attributes.get_picture_size().into(), + }) + } +} + +impl From<&[PlaylistFormatAttributeMessage]> for PlaylistFormatAttribute { + fn from(attributes: &[PlaylistFormatAttributeMessage]) -> Self { + let format_attributes = attributes + .iter() + .map(|attribute| { + ( + attribute.get_key().to_owned(), + attribute.get_value().to_owned(), + ) + }) + .collect(); + + PlaylistFormatAttribute(format_attributes) + } +} + +impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes { + type Error = MetadataError; + fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result { + Ok(Self { + added_by: attributes.get_added_by().to_owned(), + timestamp: attributes.get_timestamp().try_into()?, + seen_at: attributes.get_seen_at().try_into()?, + is_public: attributes.get_public(), + format_attributes: attributes.get_format_attributes().into(), + item_id: attributes.get_item_id().try_into()?, + }) + } +} +impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes { + type Error = MetadataError; + fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result { + Ok(Self { + values: attributes.get_values().try_into()?, + no_value: attributes.get_no_value().into(), + }) + } +} + +impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttributes { + type Error = MetadataError; + fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Result { + Ok(Self { + values: attributes.get_values().try_into()?, + no_value: attributes.get_no_value().into(), + }) + } +} + +impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes { + type Error = MetadataError; + fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result { + Ok(Self { + new_attributes: update.get_new_attributes().try_into()?, + old_attributes: update.get_old_attributes().try_into()?, + }) + } +} + +impl TryFrom<&PlaylistUpdateItemAttributesMessage> for PlaylistUpdateItemAttributes { + type Error = MetadataError; + fn try_from(update: &PlaylistUpdateItemAttributesMessage) -> Result { + Ok(Self { + index: update.get_index(), + new_attributes: update.get_new_attributes().try_into()?, + old_attributes: update.get_old_attributes().try_into()?, + }) + } +} diff --git a/metadata/src/playlist/diff.rs b/metadata/src/playlist/diff.rs new file mode 100644 index 00000000..080d72a1 --- /dev/null +++ b/metadata/src/playlist/diff.rs @@ -0,0 +1,29 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; + +use crate::error::MetadataError; + +use super::operation::PlaylistOperations; + +use librespot_core::spotify_id::SpotifyId; +use librespot_protocol as protocol; + +use protocol::playlist4_external::Diff as DiffMessage; + +#[derive(Debug, Clone)] +pub struct PlaylistDiff { + pub from_revision: SpotifyId, + pub operations: PlaylistOperations, + pub to_revision: SpotifyId, +} + +impl TryFrom<&DiffMessage> for PlaylistDiff { + type Error = MetadataError; + fn try_from(diff: &DiffMessage) -> Result { + Ok(Self { + from_revision: diff.get_from_revision().try_into()?, + operations: diff.get_ops().try_into()?, + to_revision: diff.get_to_revision().try_into()?, + }) + } +} diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs new file mode 100644 index 00000000..975a9840 --- /dev/null +++ b/metadata/src/playlist/item.rs @@ -0,0 +1,96 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; +use std::ops::Deref; + +use crate::{date::Date, error::MetadataError, util::try_from_repeated_message}; + +use super::attribute::{PlaylistAttributes, PlaylistItemAttributes}; + +use librespot_core::spotify_id::SpotifyId; +use librespot_protocol as protocol; + +use protocol::playlist4_external::Item as PlaylistItemMessage; +use protocol::playlist4_external::ListItems as PlaylistItemsMessage; +use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage; + +#[derive(Debug, Clone)] +pub struct PlaylistItem { + pub id: SpotifyId, + pub attributes: PlaylistItemAttributes, +} + +#[derive(Debug, Clone)] +pub struct PlaylistItems(pub Vec); + +impl Deref for PlaylistItems { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct PlaylistItemList { + pub position: i32, + pub is_truncated: bool, + pub items: PlaylistItems, + pub meta_items: PlaylistMetaItems, +} + +#[derive(Debug, Clone)] +pub struct PlaylistMetaItem { + pub revision: SpotifyId, + pub attributes: PlaylistAttributes, + pub length: i32, + pub timestamp: Date, + pub owner_username: String, +} + +#[derive(Debug, Clone)] +pub struct PlaylistMetaItems(pub Vec); + +impl Deref for PlaylistMetaItems { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TryFrom<&PlaylistItemMessage> for PlaylistItem { + type Error = MetadataError; + fn try_from(item: &PlaylistItemMessage) -> Result { + Ok(Self { + id: item.try_into()?, + attributes: item.get_attributes().try_into()?, + }) + } +} + +try_from_repeated_message!(PlaylistItemMessage, PlaylistItems); + +impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList { + type Error = MetadataError; + fn try_from(list_items: &PlaylistItemsMessage) -> Result { + Ok(Self { + position: list_items.get_pos(), + is_truncated: list_items.get_truncated(), + items: list_items.get_items().try_into()?, + meta_items: list_items.get_meta_items().try_into()?, + }) + } +} + +impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem { + type Error = MetadataError; + fn try_from(item: &PlaylistMetaItemMessage) -> Result { + Ok(Self { + revision: item.try_into()?, + attributes: item.get_attributes().try_into()?, + length: item.get_length(), + timestamp: item.get_timestamp().try_into()?, + owner_username: item.get_owner_username().to_owned(), + }) + } +} + +try_from_repeated_message!(PlaylistMetaItemMessage, PlaylistMetaItems); diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs new file mode 100644 index 00000000..7b5f0121 --- /dev/null +++ b/metadata/src/playlist/list.rs @@ -0,0 +1,201 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; +use std::ops::Deref; + +use protobuf::Message; + +use crate::{ + date::Date, + error::MetadataError, + request::{MercuryRequest, RequestResult}, + util::try_from_repeated_message, + Metadata, +}; + +use super::{attribute::PlaylistAttributes, diff::PlaylistDiff, item::PlaylistItemList}; + +use librespot_core::session::Session; +use librespot_core::spotify_id::{NamedSpotifyId, SpotifyId}; +use librespot_protocol as protocol; + +#[derive(Debug, Clone)] +pub struct Playlist { + pub id: NamedSpotifyId, + pub revision: SpotifyId, + pub length: i32, + pub attributes: PlaylistAttributes, + pub contents: PlaylistItemList, + pub diff: PlaylistDiff, + pub sync_result: PlaylistDiff, + pub resulting_revisions: Playlists, + pub has_multiple_heads: bool, + pub is_up_to_date: bool, + pub nonces: Vec, + pub timestamp: Date, + pub has_abuse_reporting: bool, +} + +#[derive(Debug, Clone)] +pub struct Playlists(pub Vec); + +impl Deref for Playlists { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct RootPlaylist(pub SelectedListContent); + +impl Deref for RootPlaylist { + type Target = SelectedListContent; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct SelectedListContent { + pub revision: SpotifyId, + pub length: i32, + pub attributes: PlaylistAttributes, + pub contents: PlaylistItemList, + pub diff: PlaylistDiff, + pub sync_result: PlaylistDiff, + pub resulting_revisions: Playlists, + pub has_multiple_heads: bool, + pub is_up_to_date: bool, + pub nonces: Vec, + pub timestamp: Date, + pub owner_username: String, + pub has_abuse_reporting: bool, +} + +impl Playlist { + #[allow(dead_code)] + async fn request_for_user( + session: &Session, + username: &str, + playlist_id: SpotifyId, + ) -> RequestResult { + let uri = format!( + "hm://playlist/user/{}/playlist/{}", + username, + playlist_id.to_base62() + ); + ::request(session, &uri).await + } + + #[allow(dead_code)] + pub async fn get_for_user( + session: &Session, + username: &str, + playlist_id: SpotifyId, + ) -> Result { + let response = Self::request_for_user(session, username, playlist_id).await?; + let msg = ::Message::parse_from_bytes(&response)?; + Self::parse(&msg, playlist_id) + } + + pub fn tracks(&self) -> Vec { + let tracks = self + .contents + .items + .iter() + .map(|item| item.id) + .collect::>(); + + let length = tracks.len(); + let expected_length = self.length as usize; + if length != expected_length { + warn!( + "Got {} tracks, but the list should contain {} tracks.", + length, expected_length, + ); + } + + tracks + } + + pub fn name(&self) -> &str { + &self.attributes.name + } +} + +impl MercuryRequest for Playlist {} + +#[async_trait] +impl Metadata for Playlist { + type Message = protocol::playlist4_external::SelectedListContent; + + async fn request(session: &Session, playlist_id: SpotifyId) -> RequestResult { + let uri = format!("hm://playlist/v2/playlist/{}", playlist_id.to_base62()); + ::request(session, &uri).await + } + + fn parse(msg: &Self::Message, id: SpotifyId) -> Result { + // the playlist proto doesn't contain the id so we decorate it + let playlist = SelectedListContent::try_from(msg)?; + let id = NamedSpotifyId::from_spotify_id(id, playlist.owner_username); + + Ok(Self { + id, + revision: playlist.revision, + length: playlist.length, + attributes: playlist.attributes, + contents: playlist.contents, + diff: playlist.diff, + sync_result: playlist.sync_result, + resulting_revisions: playlist.resulting_revisions, + has_multiple_heads: playlist.has_multiple_heads, + is_up_to_date: playlist.is_up_to_date, + nonces: playlist.nonces, + timestamp: playlist.timestamp, + has_abuse_reporting: playlist.has_abuse_reporting, + }) + } +} + +impl MercuryRequest for RootPlaylist {} + +impl RootPlaylist { + #[allow(dead_code)] + async fn request_for_user(session: &Session, username: &str) -> RequestResult { + let uri = format!("hm://playlist/user/{}/rootlist", username,); + ::request(session, &uri).await + } + + #[allow(dead_code)] + pub async fn get_root_for_user( + session: &Session, + username: &str, + ) -> Result { + let response = Self::request_for_user(session, username).await?; + let msg = protocol::playlist4_external::SelectedListContent::parse_from_bytes(&response)?; + Ok(Self(SelectedListContent::try_from(&msg)?)) + } +} + +impl TryFrom<&::Message> for SelectedListContent { + type Error = MetadataError; + fn try_from(playlist: &::Message) -> Result { + Ok(Self { + revision: playlist.get_revision().try_into()?, + length: playlist.get_length(), + attributes: playlist.get_attributes().try_into()?, + contents: playlist.get_contents().try_into()?, + diff: playlist.get_diff().try_into()?, + sync_result: playlist.get_sync_result().try_into()?, + resulting_revisions: playlist.get_resulting_revisions().try_into()?, + has_multiple_heads: playlist.get_multiple_heads(), + is_up_to_date: playlist.get_up_to_date(), + nonces: playlist.get_nonces().into(), + timestamp: playlist.get_timestamp().try_into()?, + owner_username: playlist.get_owner_username().to_owned(), + has_abuse_reporting: playlist.get_abuse_reporting_enabled(), + }) + } +} + +try_from_repeated_message!(Vec, Playlists); diff --git a/metadata/src/playlist/mod.rs b/metadata/src/playlist/mod.rs new file mode 100644 index 00000000..c52e637b --- /dev/null +++ b/metadata/src/playlist/mod.rs @@ -0,0 +1,9 @@ +pub mod annotation; +pub mod attribute; +pub mod diff; +pub mod item; +pub mod list; +pub mod operation; + +pub use annotation::PlaylistAnnotation; +pub use list::Playlist; diff --git a/metadata/src/playlist/operation.rs b/metadata/src/playlist/operation.rs new file mode 100644 index 00000000..c6ffa785 --- /dev/null +++ b/metadata/src/playlist/operation.rs @@ -0,0 +1,114 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; +use std::ops::Deref; + +use crate::{ + error::MetadataError, + playlist::{ + attribute::{PlaylistUpdateAttributes, PlaylistUpdateItemAttributes}, + item::PlaylistItems, + }, + util::try_from_repeated_message, +}; + +use librespot_protocol as protocol; + +use protocol::playlist4_external::Add as PlaylistAddMessage; +use protocol::playlist4_external::Mov as PlaylistMoveMessage; +use protocol::playlist4_external::Op as PlaylistOperationMessage; +use protocol::playlist4_external::Rem as PlaylistRemoveMessage; + +pub use protocol::playlist4_external::Op_Kind as PlaylistOperationKind; + +#[derive(Debug, Clone)] +pub struct PlaylistOperation { + pub kind: PlaylistOperationKind, + pub add: PlaylistOperationAdd, + pub rem: PlaylistOperationRemove, + pub mov: PlaylistOperationMove, + pub update_item_attributes: PlaylistUpdateItemAttributes, + pub update_list_attributes: PlaylistUpdateAttributes, +} + +#[derive(Debug, Clone)] +pub struct PlaylistOperations(pub Vec); + +impl Deref for PlaylistOperations { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct PlaylistOperationAdd { + pub from_index: i32, + pub items: PlaylistItems, + pub add_last: bool, + pub add_first: bool, +} + +#[derive(Debug, Clone)] +pub struct PlaylistOperationMove { + pub from_index: i32, + pub length: i32, + pub to_index: i32, +} + +#[derive(Debug, Clone)] +pub struct PlaylistOperationRemove { + pub from_index: i32, + pub length: i32, + pub items: PlaylistItems, + pub has_items_as_key: bool, +} + +impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation { + type Error = MetadataError; + fn try_from(operation: &PlaylistOperationMessage) -> Result { + Ok(Self { + kind: operation.get_kind(), + add: operation.get_add().try_into()?, + rem: operation.get_rem().try_into()?, + mov: operation.get_mov().into(), + update_item_attributes: operation.get_update_item_attributes().try_into()?, + update_list_attributes: operation.get_update_list_attributes().try_into()?, + }) + } +} + +try_from_repeated_message!(PlaylistOperationMessage, PlaylistOperations); + +impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd { + type Error = MetadataError; + fn try_from(add: &PlaylistAddMessage) -> Result { + Ok(Self { + from_index: add.get_from_index(), + items: add.get_items().try_into()?, + add_last: add.get_add_last(), + add_first: add.get_add_first(), + }) + } +} + +impl From<&PlaylistMoveMessage> for PlaylistOperationMove { + fn from(mov: &PlaylistMoveMessage) -> Self { + Self { + from_index: mov.get_from_index(), + length: mov.get_length(), + to_index: mov.get_to_index(), + } + } +} + +impl TryFrom<&PlaylistRemoveMessage> for PlaylistOperationRemove { + type Error = MetadataError; + fn try_from(remove: &PlaylistRemoveMessage) -> Result { + Ok(Self { + from_index: remove.get_from_index(), + length: remove.get_length(), + items: remove.get_items().try_into()?, + has_items_as_key: remove.get_items_as_key(), + }) + } +} diff --git a/metadata/src/request.rs b/metadata/src/request.rs new file mode 100644 index 00000000..4e47fc38 --- /dev/null +++ b/metadata/src/request.rs @@ -0,0 +1,20 @@ +use crate::error::RequestError; + +use librespot_core::session::Session; + +pub type RequestResult = Result; + +#[async_trait] +pub trait MercuryRequest { + async fn request(session: &Session, uri: &str) -> RequestResult { + let response = session.mercury().get(uri).await?; + match response.payload.first() { + Some(data) => { + let data = data.to_vec().into(); + trace!("Received metadata: {:?}", data); + Ok(data) + } + None => Err(RequestError::Empty), + } + } +} diff --git a/metadata/src/restriction.rs b/metadata/src/restriction.rs new file mode 100644 index 00000000..588e45e2 --- /dev/null +++ b/metadata/src/restriction.rs @@ -0,0 +1,106 @@ +use std::fmt::Debug; +use std::ops::Deref; + +use crate::util::{from_repeated_enum, from_repeated_message}; + +use librespot_protocol as protocol; + +use protocol::metadata::Restriction as RestrictionMessage; + +pub use protocol::metadata::Restriction_Catalogue as RestrictionCatalogue; +pub use protocol::metadata::Restriction_Type as RestrictionType; + +#[derive(Debug, Clone)] +pub struct Restriction { + pub catalogues: RestrictionCatalogues, + pub restriction_type: RestrictionType, + pub catalogue_strs: Vec, + pub countries_allowed: Option>, + pub countries_forbidden: Option>, +} + +#[derive(Debug, Clone)] +pub struct Restrictions(pub Vec); + +impl Deref for Restrictions { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct RestrictionCatalogues(pub Vec); + +impl Deref for RestrictionCatalogues { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Restriction { + fn parse_country_codes(country_codes: &str) -> Vec { + country_codes + .chunks(2) + .map(|country_code| country_code.to_owned()) + .collect() + } +} + +impl From<&RestrictionMessage> for Restriction { + fn from(restriction: &RestrictionMessage) -> Self { + let countries_allowed = if restriction.has_countries_allowed() { + Some(Self::parse_country_codes( + restriction.get_countries_allowed(), + )) + } else { + None + }; + + let countries_forbidden = if restriction.has_countries_forbidden() { + Some(Self::parse_country_codes( + restriction.get_countries_forbidden(), + )) + } else { + None + }; + + Self { + catalogues: restriction.get_catalogue().into(), + restriction_type: restriction.get_field_type(), + catalogue_strs: restriction.get_catalogue_str().to_vec(), + countries_allowed, + countries_forbidden, + } + } +} + +from_repeated_message!(RestrictionMessage, Restrictions); +from_repeated_enum!(RestrictionCatalogue, RestrictionCatalogues); + +struct StrChunks<'s>(&'s str, usize); + +trait StrChunksExt { + fn chunks(&self, size: usize) -> StrChunks; +} + +impl StrChunksExt for str { + fn chunks(&self, size: usize) -> StrChunks { + StrChunks(self, size) + } +} + +impl<'s> Iterator for StrChunks<'s> { + type Item = &'s str; + fn next(&mut self) -> Option<&'s str> { + let &mut StrChunks(data, size) = self; + if data.is_empty() { + None + } else { + let ret = Some(&data[..size]); + self.0 = &data[size..]; + ret + } + } +} diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs new file mode 100644 index 00000000..6152b901 --- /dev/null +++ b/metadata/src/sale_period.rs @@ -0,0 +1,37 @@ +use std::fmt::Debug; +use std::ops::Deref; + +use crate::{date::Date, restriction::Restrictions, util::from_repeated_message}; + +use librespot_protocol as protocol; + +use protocol::metadata::SalePeriod as SalePeriodMessage; + +#[derive(Debug, Clone)] +pub struct SalePeriod { + pub restrictions: Restrictions, + pub start: Date, + pub end: Date, +} + +#[derive(Debug, Clone)] +pub struct SalePeriods(pub Vec); + +impl Deref for SalePeriods { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&SalePeriodMessage> for SalePeriod { + fn from(sale_period: &SalePeriodMessage) -> Self { + Self { + restrictions: sale_period.get_restriction().into(), + start: sale_period.get_start().into(), + end: sale_period.get_end().into(), + } + } +} + +from_repeated_message!(SalePeriodMessage, SalePeriods); diff --git a/metadata/src/show.rs b/metadata/src/show.rs new file mode 100644 index 00000000..4e75c598 --- /dev/null +++ b/metadata/src/show.rs @@ -0,0 +1,75 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; + +use crate::{ + availability::Availabilities, copyright::Copyrights, episode::Episodes, error::RequestError, + image::Images, restriction::Restrictions, Metadata, MetadataError, RequestResult, +}; + +use librespot_core::session::Session; +use librespot_core::spotify_id::SpotifyId; +use librespot_protocol as protocol; + +pub use protocol::metadata::Show_ConsumptionOrder as ShowConsumptionOrder; +pub use protocol::metadata::Show_MediaType as ShowMediaType; + +#[derive(Debug, Clone)] +pub struct Show { + pub id: SpotifyId, + pub name: String, + pub description: String, + pub publisher: String, + pub language: String, + pub is_explicit: bool, + pub covers: Images, + pub episodes: Episodes, + pub copyrights: Copyrights, + pub restrictions: Restrictions, + pub keywords: Vec, + pub media_type: ShowMediaType, + pub consumption_order: ShowConsumptionOrder, + pub availability: Availabilities, + pub trailer_uri: SpotifyId, + pub has_music_and_talk: bool, +} + +#[async_trait] +impl Metadata for Show { + type Message = protocol::metadata::Show; + + async fn request(session: &Session, show_id: SpotifyId) -> RequestResult { + session + .spclient() + .get_show_metadata(show_id) + .await + .map_err(RequestError::Http) + } + + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + Self::try_from(msg) + } +} + +impl TryFrom<&::Message> for Show { + type Error = MetadataError; + fn try_from(show: &::Message) -> Result { + Ok(Self { + id: show.try_into()?, + name: show.get_name().to_owned(), + description: show.get_description().to_owned(), + publisher: show.get_publisher().to_owned(), + language: show.get_language().to_owned(), + is_explicit: show.get_explicit(), + covers: show.get_cover_image().get_image().into(), + episodes: show.get_episode().try_into()?, + copyrights: show.get_copyright().into(), + restrictions: show.get_restriction().into(), + keywords: show.get_keyword().to_vec(), + media_type: show.get_media_type(), + consumption_order: show.get_consumption_order(), + availability: show.get_availability().into(), + trailer_uri: SpotifyId::from_uri(show.get_trailer_uri())?, + has_music_and_talk: show.get_music_and_talk(), + }) + } +} diff --git a/metadata/src/track.rs b/metadata/src/track.rs new file mode 100644 index 00000000..8e7f6702 --- /dev/null +++ b/metadata/src/track.rs @@ -0,0 +1,150 @@ +use std::convert::{TryFrom, TryInto}; +use std::fmt::Debug; +use std::ops::Deref; + +use chrono::Local; +use uuid::Uuid; + +use crate::{ + artist::{Artists, ArtistsWithRole}, + audio::{ + file::AudioFiles, + item::{AudioItem, AudioItemResult, InnerAudioItem}, + }, + availability::{Availabilities, UnavailabilityReason}, + content_rating::ContentRatings, + date::Date, + error::RequestError, + external_id::ExternalIds, + restriction::Restrictions, + sale_period::SalePeriods, + util::try_from_repeated_message, + Metadata, MetadataError, RequestResult, +}; + +use librespot_core::session::Session; +use librespot_core::spotify_id::SpotifyId; +use librespot_protocol as protocol; + +#[derive(Debug, Clone)] +pub struct Track { + pub id: SpotifyId, + pub name: String, + pub album: SpotifyId, + pub artists: Artists, + pub number: i32, + pub disc_number: i32, + pub duration: i32, + pub popularity: i32, + pub is_explicit: bool, + pub external_ids: ExternalIds, + pub restrictions: Restrictions, + pub files: AudioFiles, + pub alternatives: Tracks, + pub sale_periods: SalePeriods, + pub previews: AudioFiles, + pub tags: Vec, + pub earliest_live_timestamp: Date, + pub has_lyrics: bool, + pub availability: Availabilities, + pub licensor: Uuid, + pub language_of_performance: Vec, + pub content_ratings: ContentRatings, + pub original_title: String, + pub version_title: String, + pub artists_with_role: ArtistsWithRole, +} + +#[derive(Debug, Clone)] +pub struct Tracks(pub Vec); + +impl Deref for Tracks { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[async_trait] +impl InnerAudioItem for Track { + async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult { + let track = Self::get(session, id).await?; + let alternatives = { + if track.alternatives.is_empty() { + None + } else { + Some(track.alternatives.clone()) + } + }; + + // TODO: check meaning of earliest_live_timestamp in + let availability = if Local::now() < track.earliest_live_timestamp.as_utc() { + Err(UnavailabilityReason::Embargo) + } else { + Self::available_in_country(&track.availability, &track.restrictions, &session.country()) + }; + + Ok(AudioItem { + id, + spotify_uri: id.to_uri(), + files: track.files, + name: track.name, + duration: track.duration, + availability, + alternatives, + }) + } +} + +#[async_trait] +impl Metadata for Track { + type Message = protocol::metadata::Track; + + async fn request(session: &Session, track_id: SpotifyId) -> RequestResult { + session + .spclient() + .get_track_metadata(track_id) + .await + .map_err(RequestError::Http) + } + + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + Self::try_from(msg) + } +} + +impl TryFrom<&::Message> for Track { + type Error = MetadataError; + fn try_from(track: &::Message) -> Result { + Ok(Self { + id: track.try_into()?, + name: track.get_name().to_owned(), + album: track.get_album().try_into()?, + artists: track.get_artist().try_into()?, + number: track.get_number(), + disc_number: track.get_disc_number(), + duration: track.get_duration(), + popularity: track.get_popularity(), + is_explicit: track.get_explicit(), + external_ids: track.get_external_id().into(), + restrictions: track.get_restriction().into(), + files: track.get_file().into(), + alternatives: track.get_alternative().try_into()?, + sale_periods: track.get_sale_period().into(), + previews: track.get_preview().into(), + tags: track.get_tags().to_vec(), + earliest_live_timestamp: track.get_earliest_live_timestamp().try_into()?, + has_lyrics: track.get_has_lyrics(), + availability: track.get_availability().into(), + licensor: Uuid::from_slice(track.get_licensor().get_uuid()) + .unwrap_or_else(|_| Uuid::nil()), + language_of_performance: track.get_language_of_performance().to_vec(), + content_ratings: track.get_content_rating().into(), + original_title: track.get_original_title().to_owned(), + version_title: track.get_version_title().to_owned(), + artists_with_role: track.get_artist_with_role().try_into()?, + }) + } +} + +try_from_repeated_message!(::Message, Tracks); diff --git a/metadata/src/util.rs b/metadata/src/util.rs new file mode 100644 index 00000000..d0065221 --- /dev/null +++ b/metadata/src/util.rs @@ -0,0 +1,39 @@ +macro_rules! from_repeated_message { + ($src:ty, $dst:ty) => { + impl From<&[$src]> for $dst { + fn from(src: &[$src]) -> Self { + let result = src.iter().map(From::from).collect(); + Self(result) + } + } + }; +} + +pub(crate) use from_repeated_message; + +macro_rules! from_repeated_enum { + ($src:ty, $dst:ty) => { + impl From<&[$src]> for $dst { + fn from(src: &[$src]) -> Self { + let result = src.iter().map(|x| <$src>::from(*x)).collect(); + Self(result) + } + } + }; +} + +pub(crate) use from_repeated_enum; + +macro_rules! try_from_repeated_message { + ($src:ty, $dst:ty) => { + impl TryFrom<&[$src]> for $dst { + type Error = MetadataError; + fn try_from(src: &[$src]) -> Result { + let result: Result, _> = src.iter().map(TryFrom::try_from).collect(); + Ok(Self(result?)) + } + } + }; +} + +pub(crate) use try_from_repeated_message; diff --git a/metadata/src/video.rs b/metadata/src/video.rs new file mode 100644 index 00000000..926727a5 --- /dev/null +++ b/metadata/src/video.rs @@ -0,0 +1,21 @@ +use std::fmt::Debug; +use std::ops::Deref; + +use crate::util::from_repeated_message; + +use librespot_core::spotify_id::FileId; +use librespot_protocol as protocol; + +use protocol::metadata::VideoFile as VideoFileMessage; + +#[derive(Debug, Clone)] +pub struct VideoFiles(pub Vec); + +impl Deref for VideoFiles { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +from_repeated_message!(VideoFileMessage, VideoFiles); diff --git a/playback/src/player.rs b/playback/src/player.rs index 1395b99a..61c7105a 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -24,7 +24,7 @@ use crate::core::session::Session; use crate::core::spotify_id::SpotifyId; use crate::core::util::SeqGenerator; use crate::decoder::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder}; -use crate::metadata::{AudioItem, FileFormat}; +use crate::metadata::audio::{AudioFileFormat, AudioItem}; use crate::mixer::AudioFilter; use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND}; @@ -639,17 +639,17 @@ struct PlayerTrackLoader { impl PlayerTrackLoader { async fn find_available_alternative(&self, audio: AudioItem) -> Option { - if audio.available { + if audio.availability.is_ok() { Some(audio) } else if let Some(alternatives) = &audio.alternatives { let alternatives: FuturesUnordered<_> = alternatives .iter() - .map(|alt_id| AudioItem::get_audio_item(&self.session, *alt_id)) + .map(|alt_id| AudioItem::get_file(&self.session, *alt_id)) .collect(); alternatives .filter_map(|x| future::ready(x.ok())) - .filter(|x| future::ready(x.available)) + .filter(|x| future::ready(x.availability.is_ok())) .next() .await } else { @@ -657,19 +657,19 @@ impl PlayerTrackLoader { } } - fn stream_data_rate(&self, format: FileFormat) -> usize { + fn stream_data_rate(&self, format: AudioFileFormat) -> usize { match format { - FileFormat::OGG_VORBIS_96 => 12 * 1024, - FileFormat::OGG_VORBIS_160 => 20 * 1024, - FileFormat::OGG_VORBIS_320 => 40 * 1024, - FileFormat::MP3_256 => 32 * 1024, - FileFormat::MP3_320 => 40 * 1024, - FileFormat::MP3_160 => 20 * 1024, - FileFormat::MP3_96 => 12 * 1024, - FileFormat::MP3_160_ENC => 20 * 1024, - FileFormat::AAC_24 => 3 * 1024, - FileFormat::AAC_48 => 6 * 1024, - FileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average + AudioFileFormat::OGG_VORBIS_96 => 12 * 1024, + AudioFileFormat::OGG_VORBIS_160 => 20 * 1024, + AudioFileFormat::OGG_VORBIS_320 => 40 * 1024, + AudioFileFormat::MP3_256 => 32 * 1024, + AudioFileFormat::MP3_320 => 40 * 1024, + AudioFileFormat::MP3_160 => 20 * 1024, + AudioFileFormat::MP3_96 => 12 * 1024, + AudioFileFormat::MP3_160_ENC => 20 * 1024, + AudioFileFormat::AAC_24 => 3 * 1024, + AudioFileFormat::AAC_48 => 6 * 1024, + AudioFileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average } } @@ -678,7 +678,7 @@ impl PlayerTrackLoader { spotify_id: SpotifyId, position_ms: u32, ) -> Option { - let audio = match AudioItem::get_audio_item(&self.session, spotify_id).await { + let audio = match AudioItem::get_file(&self.session, spotify_id).await { Ok(audio) => audio, Err(_) => { error!("Unable to load audio item."); @@ -686,7 +686,10 @@ impl PlayerTrackLoader { } }; - info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri); + info!( + "Loading <{}> with Spotify URI <{}>", + audio.name, audio.spotify_uri + ); let audio = match self.find_available_alternative(audio).await { Some(audio) => audio, @@ -699,22 +702,23 @@ impl PlayerTrackLoader { assert!(audio.duration >= 0); let duration_ms = audio.duration as u32; - // (Most) podcasts seem to support only 96 bit Vorbis, so fall back to it + // (Most) podcasts seem to support only 96 kbps Vorbis, so fall back to it + // TODO: update this logic once we also support MP3 and/or FLAC let formats = match self.config.bitrate { Bitrate::Bitrate96 => [ - FileFormat::OGG_VORBIS_96, - FileFormat::OGG_VORBIS_160, - FileFormat::OGG_VORBIS_320, + AudioFileFormat::OGG_VORBIS_96, + AudioFileFormat::OGG_VORBIS_160, + AudioFileFormat::OGG_VORBIS_320, ], Bitrate::Bitrate160 => [ - FileFormat::OGG_VORBIS_160, - FileFormat::OGG_VORBIS_96, - FileFormat::OGG_VORBIS_320, + AudioFileFormat::OGG_VORBIS_160, + AudioFileFormat::OGG_VORBIS_96, + AudioFileFormat::OGG_VORBIS_320, ], Bitrate::Bitrate320 => [ - FileFormat::OGG_VORBIS_320, - FileFormat::OGG_VORBIS_160, - FileFormat::OGG_VORBIS_96, + AudioFileFormat::OGG_VORBIS_320, + AudioFileFormat::OGG_VORBIS_160, + AudioFileFormat::OGG_VORBIS_96, ], }; From 87f6a78d3ebbb2e291f560ee51306128899c2713 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 7 Dec 2021 23:52:34 +0100 Subject: [PATCH 58/95] Fix examples --- examples/playlist_tracks.rs | 2 +- metadata/src/lib.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 75c656bb..0b19e73e 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -30,7 +30,7 @@ async fn main() { let plist = Playlist::get(&session, plist_uri).await.unwrap(); println!("{:?}", plist); - for track_id in plist.tracks { + for track_id in plist.tracks() { let plist_track = Track::get(&session, track_id).await.unwrap(); println!("track: {} ", plist_track.name); } diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index f1090b0f..3f1849b5 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -29,9 +29,16 @@ pub mod track; mod util; pub mod video; -use error::MetadataError; +pub use error::MetadataError; use request::RequestResult; +pub use album::Album; +pub use artist::Artist; +pub use episode::Episode; +pub use playlist::Playlist; +pub use show::Show; +pub use track::Track; + #[async_trait] pub trait Metadata: Send + Sized + 'static { type Message: protobuf::Message; From 9b2ca1442e1bbb0beca81dd85c09750239c874c7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 8 Dec 2021 19:53:45 +0100 Subject: [PATCH 59/95] Move FileId out of SpotifyId --- audio/src/fetch/mod.rs | 8 ++-- audio/src/fetch/receive.rs | 9 ++-- core/src/audio_key.rs | 3 +- core/src/cache.rs | 2 +- core/src/file_id.rs | 55 ++++++++++++++++++++++++ core/src/lib.rs | 1 + core/src/spclient.rs | 3 +- core/src/spotify_id.rs | 86 ++++++++++++++----------------------- metadata/src/audio/file.rs | 2 +- metadata/src/external_id.rs | 2 +- metadata/src/image.rs | 3 +- metadata/src/video.rs | 2 +- 12 files changed, 108 insertions(+), 68 deletions(-) create mode 100644 core/src/file_id.rs diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 636194a8..5ff3db8a 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -9,13 +9,15 @@ use std::time::{Duration, Instant}; use byteorder::{BigEndian, ByteOrder}; use futures_util::{future, StreamExt, TryFutureExt, TryStreamExt}; -use librespot_core::channel::{ChannelData, ChannelError, ChannelHeaders}; -use librespot_core::session::Session; -use librespot_core::spotify_id::FileId; use tempfile::NamedTempFile; use tokio::sync::{mpsc, oneshot}; +use librespot_core::channel::{ChannelData, ChannelError, ChannelHeaders}; +use librespot_core::file_id::FileId; +use librespot_core::session::Session; + use self::receive::{audio_file_fetch, request_range}; + use crate::range_set::{Range, RangeSet}; /// The minimum size of a block that is requested from the Spotify servers in one request. diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index d57e6cc4..61a86953 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -7,13 +7,14 @@ use atomic::Ordering; use byteorder::{BigEndian, WriteBytesExt}; use bytes::Bytes; use futures_util::StreamExt; -use librespot_core::channel::{Channel, ChannelData}; -use librespot_core::packet::PacketType; -use librespot_core::session::Session; -use librespot_core::spotify_id::FileId; use tempfile::NamedTempFile; use tokio::sync::{mpsc, oneshot}; +use librespot_core::channel::{Channel, ChannelData}; +use librespot_core::file_id::FileId; +use librespot_core::packet::PacketType; +use librespot_core::session::Session; + use crate::range_set::{Range, RangeSet}; use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand}; diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index f42c6502..2198819e 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -4,8 +4,9 @@ use std::collections::HashMap; use std::io::Write; use tokio::sync::oneshot; +use crate::file_id::FileId; use crate::packet::PacketType; -use crate::spotify_id::{FileId, SpotifyId}; +use crate::spotify_id::SpotifyId; use crate::util::SeqGenerator; #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] diff --git a/core/src/cache.rs b/core/src/cache.rs index da2ad022..7d85bd6a 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -9,7 +9,7 @@ use std::time::SystemTime; use priority_queue::PriorityQueue; use crate::authentication::Credentials; -use crate::spotify_id::FileId; +use crate::file_id::FileId; /// Some kind of data structure that holds some paths, the size of these files and a timestamp. /// It keeps track of the file sizes and is able to pop the path with the oldest timestamp if diff --git a/core/src/file_id.rs b/core/src/file_id.rs new file mode 100644 index 00000000..f6e385cd --- /dev/null +++ b/core/src/file_id.rs @@ -0,0 +1,55 @@ +use librespot_protocol as protocol; + +use std::fmt; + +use crate::spotify_id::to_base16; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FileId(pub [u8; 20]); + +impl FileId { + pub fn from_raw(src: &[u8]) -> FileId { + let mut dst = [0u8; 20]; + dst.clone_from_slice(src); + FileId(dst) + } + + pub fn to_base16(&self) -> String { + to_base16(&self.0, &mut [0u8; 40]) + } +} + +impl fmt::Debug for FileId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("FileId").field(&self.to_base16()).finish() + } +} + +impl fmt::Display for FileId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.to_base16()) + } +} + +impl From<&[u8]> for FileId { + fn from(src: &[u8]) -> Self { + Self::from_raw(src) + } +} +impl From<&protocol::metadata::Image> for FileId { + fn from(image: &protocol::metadata::Image) -> Self { + Self::from(image.get_file_id()) + } +} + +impl From<&protocol::metadata::AudioFile> for FileId { + fn from(file: &protocol::metadata::AudioFile) -> Self { + Self::from(file.get_file_id()) + } +} + +impl From<&protocol::metadata::VideoFile> for FileId { + fn from(video: &protocol::metadata::VideoFile) -> Self { + Self::from(video.get_file_id()) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index c928f32b..09275d80 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -18,6 +18,7 @@ mod connection; mod dealer; #[doc(hidden)] pub mod diffie_hellman; +pub mod file_id; mod http_client; pub mod mercury; pub mod packet; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index a3bfe9c5..7e74d75b 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -1,10 +1,11 @@ use crate::apresolve::SocketAddress; +use crate::file_id::FileId; use crate::http_client::HttpClientError; use crate::mercury::MercuryError; use crate::protocol::canvaz::EntityCanvazRequest; use crate::protocol::connect::PutStateRequest; use crate::protocol::extended_metadata::BatchedEntityRequest; -use crate::spotify_id::{FileId, SpotifyId}; +use crate::spotify_id::SpotifyId; use bytes::Bytes; use http::header::HeaderValue; diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index c03382a2..9f6d92ed 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -6,6 +6,9 @@ use std::convert::{TryFrom, TryInto}; use std::fmt; use std::ops::Deref; +// re-export FileId for historic reasons, when it was part of this mod +pub use crate::file_id::FileId; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SpotifyItemType { Album, @@ -45,7 +48,7 @@ impl From for &str { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct SpotifyId { pub id: u128, pub item_type: SpotifyItemType, @@ -258,7 +261,19 @@ impl SpotifyId { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +impl fmt::Debug for SpotifyId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("SpotifyId").field(&self.to_uri()).finish() + } +} + +impl fmt::Display for SpotifyId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.to_uri()) + } +} + +#[derive(Clone, PartialEq, Eq, Hash)] pub struct NamedSpotifyId { pub inner_id: SpotifyId, pub username: String, @@ -314,6 +329,20 @@ impl Deref for NamedSpotifyId { } } +impl fmt::Debug for NamedSpotifyId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("NamedSpotifyId") + .field(&self.inner_id.to_uri()) + .finish() + } +} + +impl fmt::Display for NamedSpotifyId { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.inner_id.to_uri()) + } +} + impl TryFrom<&[u8]> for SpotifyId { type Error = SpotifyIdError; fn try_from(src: &[u8]) -> Result { @@ -456,58 +485,7 @@ impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId { } } -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct FileId(pub [u8; 20]); - -impl FileId { - pub fn from_raw(src: &[u8]) -> FileId { - let mut dst = [0u8; 20]; - dst.clone_from_slice(src); - FileId(dst) - } - - pub fn to_base16(&self) -> String { - to_base16(&self.0, &mut [0u8; 40]) - } -} - -impl fmt::Debug for FileId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_tuple("FileId").field(&self.to_base16()).finish() - } -} - -impl fmt::Display for FileId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&self.to_base16()) - } -} - -impl From<&[u8]> for FileId { - fn from(src: &[u8]) -> Self { - Self::from_raw(src) - } -} -impl From<&protocol::metadata::Image> for FileId { - fn from(image: &protocol::metadata::Image) -> Self { - Self::from(image.get_file_id()) - } -} - -impl From<&protocol::metadata::AudioFile> for FileId { - fn from(file: &protocol::metadata::AudioFile) -> Self { - Self::from(file.get_file_id()) - } -} - -impl From<&protocol::metadata::VideoFile> for FileId { - fn from(video: &protocol::metadata::VideoFile) -> Self { - Self::from(video.get_file_id()) - } -} - -#[inline] -fn to_base16(src: &[u8], buf: &mut [u8]) -> String { +pub fn to_base16(src: &[u8], buf: &mut [u8]) -> String { let mut i = 0; for v in src { buf[i] = BASE16_DIGITS[(v >> 4) as usize]; diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs index 01ec984e..fd202a40 100644 --- a/metadata/src/audio/file.rs +++ b/metadata/src/audio/file.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt::Debug; use std::ops::Deref; -use librespot_core::spotify_id::FileId; +use librespot_core::file_id::FileId; use librespot_protocol as protocol; use protocol::metadata::AudioFile as AudioFileMessage; diff --git a/metadata/src/external_id.rs b/metadata/src/external_id.rs index 31755e72..5da45634 100644 --- a/metadata/src/external_id.rs +++ b/metadata/src/external_id.rs @@ -10,7 +10,7 @@ use protocol::metadata::ExternalId as ExternalIdMessage; #[derive(Debug, Clone)] pub struct ExternalId { pub external_type: String, - pub id: String, + pub id: String, // this can be anything from a URL to a ISRC, EAN or UPC } #[derive(Debug, Clone)] diff --git a/metadata/src/image.rs b/metadata/src/image.rs index b6653d09..345722c9 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -7,7 +7,8 @@ use crate::{ util::{from_repeated_message, try_from_repeated_message}, }; -use librespot_core::spotify_id::{FileId, SpotifyId}; +use librespot_core::file_id::FileId; +use librespot_core::spotify_id::SpotifyId; use librespot_protocol as protocol; use protocol::metadata::Image as ImageMessage; diff --git a/metadata/src/video.rs b/metadata/src/video.rs index 926727a5..83f653bb 100644 --- a/metadata/src/video.rs +++ b/metadata/src/video.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use crate::util::from_repeated_message; -use librespot_core::spotify_id::FileId; +use librespot_core::file_id::FileId; use librespot_protocol as protocol; use protocol::metadata::VideoFile as VideoFileMessage; From f74c574c9fc848e0c98ee06a911a763cd78aa868 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 8 Dec 2021 20:27:15 +0100 Subject: [PATCH 60/95] Fix lyrics and add simpler endpoint --- core/src/spclient.rs | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 7e74d75b..3a40c1a7 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -84,7 +84,7 @@ impl SpClient { format!("https://{}:{}", ap.0, ap.1) } - pub async fn protobuf_request( + pub async fn request_with_protobuf( &self, method: &str, endpoint: &str, @@ -100,6 +100,19 @@ impl SpClient { .await } + pub async fn request_as_json( + &self, + method: &str, + endpoint: &str, + headers: Option, + body: Option, + ) -> SpClientResult { + let mut headers = headers.unwrap_or_else(HeaderMap::new); + headers.insert("Accept", "application/json".parse()?); + + self.request(method, endpoint, Some(headers), body).await + } + pub async fn request( &self, method: &str, @@ -199,7 +212,7 @@ impl SpClient { let mut headers = HeaderMap::new(); headers.insert("X-Spotify-Connection-Id", connection_id.parse()?); - self.protobuf_request("PUT", &endpoint, Some(headers), &state) + self.request_with_protobuf("PUT", &endpoint, Some(headers), &state) .await } @@ -228,29 +241,36 @@ impl SpClient { self.get_metadata("show", show_id).await } - pub async fn get_lyrics(&self, track_id: SpotifyId, image_id: FileId) -> SpClientResult { + pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult { + let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62(),); + + self.request_as_json("GET", &endpoint, None, None).await + } + + pub async fn get_lyrics_for_image( + &self, + track_id: SpotifyId, + image_id: FileId, + ) -> SpClientResult { let endpoint = format!( "/color-lyrics/v2/track/{}/image/spotify:image:{}", - track_id.to_base16(), + track_id.to_base62(), image_id ); - let mut headers = HeaderMap::new(); - headers.insert("Content-Type", "application/json".parse()?); - - self.request("GET", &endpoint, Some(headers), None).await + self.request_as_json("GET", &endpoint, None, None).await } // TODO: Find endpoint for newer canvas.proto and upgrade to that. pub async fn get_canvases(&self, request: EntityCanvazRequest) -> SpClientResult { let endpoint = "/canvaz-cache/v0/canvases"; - self.protobuf_request("POST", endpoint, None, &request) + self.request_with_protobuf("POST", endpoint, None, &request) .await } pub async fn get_extended_metadata(&self, request: BatchedEntityRequest) -> SpClientResult { let endpoint = "/extended-metadata/v0/extended-metadata"; - self.protobuf_request("POST", endpoint, None, &request) + self.request_with_protobuf("POST", endpoint, None, &request) .await } } From 33620280f566ce58c513639baf35d805457e7722 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 8 Dec 2021 20:44:24 +0100 Subject: [PATCH 61/95] Fix build on Cargo 1.48 --- Cargo.lock | 44 +-------------------------------- discovery/Cargo.toml | 1 - discovery/examples/discovery.rs | 6 ----- discovery/src/lib.rs | 2 -- 4 files changed, 1 insertion(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index daf7ce62..d4501fef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,7 +213,7 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time 0.1.43", + "time", "winapi", ] @@ -237,17 +237,6 @@ dependencies = [ "libloading 0.7.2", ] -[[package]] -name = "colored" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - [[package]] name = "combine" version = "4.6.2" @@ -1316,7 +1305,6 @@ dependencies = [ "rand", "serde_json", "sha-1", - "simple_logger", "thiserror", "tokio", ] @@ -2297,19 +2285,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simple_logger" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "205596cf77a15774e5601c5ef759f4211ac381c0855a1f1d5e24a46f60f93e9a" -dependencies = [ - "atty", - "colored", - "log", - "time 0.3.5", - "winapi", -] - [[package]] name = "slab" version = "0.4.5" @@ -2465,23 +2440,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "time" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" -dependencies = [ - "itoa", - "libc", - "time-macros", -] - -[[package]] -name = "time-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" - [[package]] name = "tinyvec" version = "1.5.1" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 9b4d415e..368f3747 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -33,7 +33,6 @@ version = "0.3.1" [dev-dependencies] futures = "0.3" hex = "0.4" -simple_logger = "1.11" tokio = { version = "1.0", features = ["macros", "rt"] } [features] diff --git a/discovery/examples/discovery.rs b/discovery/examples/discovery.rs index cd913fd2..f7dee532 100644 --- a/discovery/examples/discovery.rs +++ b/discovery/examples/discovery.rs @@ -1,15 +1,9 @@ use futures::StreamExt; use librespot_discovery::DeviceType; use sha1::{Digest, Sha1}; -use simple_logger::SimpleLogger; #[tokio::main(flavor = "current_thread")] async fn main() { - SimpleLogger::new() - .with_level(log::LevelFilter::Debug) - .init() - .unwrap(); - let name = "Librespot"; let device_id = hex::encode(Sha1::digest(name.as_bytes())); diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index b1249a0d..98f776fb 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -7,8 +7,6 @@ //! This library uses mDNS and DNS-SD so that other devices can find it, //! and spawns an http server to answer requests of Spotify clients. -#![warn(clippy::all, missing_docs, rust_2018_idioms)] - mod server; use std::borrow::Cow; From f3bb679ab17fda484ca80f15565be5eb3bf679f2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 8 Dec 2021 21:00:42 +0100 Subject: [PATCH 62/95] Rid of the last remaining clippy warnings --- audio/src/fetch/mod.rs | 32 +++++++++++++++++++------------- audio/src/fetch/receive.rs | 17 +++++++---------- audio/src/lib.rs | 2 -- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 5ff3db8a..b68f6858 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -255,6 +255,12 @@ struct AudioFileShared { read_position: AtomicUsize, } +pub struct InitialData { + rx: ChannelData, + length: usize, + request_sent_time: Instant, +} + impl AudioFile { pub async fn open( session: &Session, @@ -270,7 +276,7 @@ impl AudioFile { debug!("Downloading file {}", file_id); let (complete_tx, complete_rx) = oneshot::channel(); - let mut initial_data_length = if play_from_beginning { + let mut length = if play_from_beginning { INITIAL_DOWNLOAD_SIZE + max( (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, @@ -281,16 +287,20 @@ impl AudioFile { } else { INITIAL_DOWNLOAD_SIZE }; - if initial_data_length % 4 != 0 { - initial_data_length += 4 - (initial_data_length % 4); + if length % 4 != 0 { + length += 4 - (length % 4); } - let (headers, data) = request_range(session, file_id, 0, initial_data_length).split(); + let (headers, rx) = request_range(session, file_id, 0, length).split(); + + let initial_data = InitialData { + rx, + length, + request_sent_time: Instant::now(), + }; let streaming = AudioFileStreaming::open( session.clone(), - data, - initial_data_length, - Instant::now(), + initial_data, headers, file_id, complete_tx, @@ -333,9 +343,7 @@ impl AudioFile { impl AudioFileStreaming { pub async fn open( session: Session, - initial_data_rx: ChannelData, - initial_data_length: usize, - initial_request_sent_time: Instant, + initial_data: InitialData, headers: ChannelHeaders, file_id: FileId, complete_tx: oneshot::Sender, @@ -377,9 +385,7 @@ impl AudioFileStreaming { session.spawn(audio_file_fetch( session.clone(), shared.clone(), - initial_data_rx, - initial_request_sent_time, - initial_data_length, + initial_data, write_file, stream_loader_command_rx, complete_tx, diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 61a86953..7b797b02 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -17,7 +17,7 @@ use librespot_core::session::Session; use crate::range_set::{Range, RangeSet}; -use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand}; +use super::{AudioFileShared, DownloadStrategy, InitialData, StreamLoaderCommand}; use super::{ FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, @@ -45,7 +45,7 @@ pub fn request_range(session: &Session, file: FileId, offset: usize, length: usi data.write_u32::(0x00000000).unwrap(); data.write_u32::(0x00009C40).unwrap(); data.write_u32::(0x00020000).unwrap(); - data.write(&file.0).unwrap(); + data.write_all(&file.0).unwrap(); data.write_u32::(start as u32).unwrap(); data.write_u32::(end as u32).unwrap(); @@ -356,10 +356,7 @@ impl AudioFileFetch { pub(super) async fn audio_file_fetch( session: Session, shared: Arc, - initial_data_rx: ChannelData, - initial_request_sent_time: Instant, - initial_data_length: usize, - + initial_data: InitialData, output: NamedTempFile, mut stream_loader_command_rx: mpsc::UnboundedReceiver, complete_tx: oneshot::Sender, @@ -367,7 +364,7 @@ pub(super) async fn audio_file_fetch( let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel(); { - let requested_range = Range::new(0, initial_data_length); + let requested_range = Range::new(0, initial_data.length); let mut download_status = shared.download_status.lock().unwrap(); download_status.requested.add_range(&requested_range); } @@ -375,10 +372,10 @@ pub(super) async fn audio_file_fetch( session.spawn(receive_data( shared.clone(), file_data_tx.clone(), - initial_data_rx, + initial_data.rx, 0, - initial_data_length, - initial_request_sent_time, + initial_data.length, + initial_data.request_sent_time, )); let mut fetch = AudioFileFetch { diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 4b486bbe..0c96b0d0 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::unused_io_amount, clippy::too_many_arguments)] - #[macro_use] extern crate log; From 4f51c1e810b0f53a2c90e7e36fb539e0890d3b66 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 9 Dec 2021 19:00:27 +0100 Subject: [PATCH 63/95] Report actual CPU, OS, platform and librespot version --- connect/src/spirc.rs | 2 +- core/src/connection/handshake.rs | 47 ++++++++++++++++++++++++++++++-- core/src/connection/mod.rs | 33 ++++++++++++++++++---- core/src/http_client.rs | 23 +++++++++++++++- discovery/src/server.rs | 2 +- 5 files changed, 95 insertions(+), 12 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 758025a1..e64e35a5 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -108,7 +108,7 @@ fn initial_state() -> State { fn initial_device_state(config: ConnectConfig) -> DeviceState { { let mut msg = DeviceState::new(); - msg.set_sw_version(version::VERSION_STRING.to_string()); + msg.set_sw_version(version::SEMVER.to_string()); msg.set_is_active(false); msg.set_can_play(true); msg.set_volume(0); diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 7194f0f4..3659ab82 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -3,6 +3,7 @@ use hmac::{Hmac, Mac, NewMac}; use protobuf::{self, Message}; use rand::{thread_rng, RngCore}; use sha1::Sha1; +use std::env::consts::ARCH; use std::io; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio_util::codec::{Decoder, Framed}; @@ -10,7 +11,9 @@ use tokio_util::codec::{Decoder, Framed}; use super::codec::ApCodec; use crate::diffie_hellman::DhLocalKeys; use crate::protocol; -use crate::protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext}; +use crate::protocol::keyexchange::{ + APResponseMessage, ClientHello, ClientResponsePlaintext, Platform, ProductFlags, +}; pub async fn handshake( mut connection: T, @@ -42,13 +45,51 @@ where let mut client_nonce = vec![0; 0x10]; thread_rng().fill_bytes(&mut client_nonce); + let platform = match std::env::consts::OS { + "android" => Platform::PLATFORM_ANDROID_ARM, + "freebsd" | "netbsd" | "openbsd" => match ARCH { + "x86_64" => Platform::PLATFORM_FREEBSD_X86_64, + _ => Platform::PLATFORM_FREEBSD_X86, + }, + "ios" => match ARCH { + "arm64" => Platform::PLATFORM_IPHONE_ARM64, + _ => Platform::PLATFORM_IPHONE_ARM, + }, + "linux" => match ARCH { + "arm" | "arm64" => Platform::PLATFORM_LINUX_ARM, + "blackfin" => Platform::PLATFORM_LINUX_BLACKFIN, + "mips" => Platform::PLATFORM_LINUX_MIPS, + "sh" => Platform::PLATFORM_LINUX_SH, + "x86_64" => Platform::PLATFORM_LINUX_X86_64, + _ => Platform::PLATFORM_LINUX_X86, + }, + "macos" => match ARCH { + "ppc" | "ppc64" => Platform::PLATFORM_OSX_PPC, + "x86_64" => Platform::PLATFORM_OSX_X86_64, + _ => Platform::PLATFORM_OSX_X86, + }, + "windows" => match ARCH { + "arm" => Platform::PLATFORM_WINDOWS_CE_ARM, + "x86_64" => Platform::PLATFORM_WIN32_X86_64, + _ => Platform::PLATFORM_WIN32_X86, + }, + _ => Platform::PLATFORM_LINUX_X86, + }; + + #[cfg(debug_assertions)] + const PRODUCT_FLAGS: ProductFlags = ProductFlags::PRODUCT_FLAG_DEV_BUILD; + #[cfg(not(debug_assertions))] + const PRODUCT_FLAGS: ProductFlags = ProductFlags::PRODUCT_FLAG_NONE; + let mut packet = ClientHello::new(); packet .mut_build_info() - .set_product(protocol::keyexchange::Product::PRODUCT_PARTNER); + .set_product(protocol::keyexchange::Product::PRODUCT_LIBSPOTIFY); packet .mut_build_info() - .set_platform(protocol::keyexchange::Platform::PLATFORM_LINUX_X86); + .mut_product_flags() + .push(PRODUCT_FLAGS); + packet.mut_build_info().set_platform(platform); packet.mut_build_info().set_version(999999999); packet .mut_cryptosuites_supported() diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 472109e6..29a33296 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -71,6 +71,29 @@ pub async fn authenticate( ) -> Result { use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; + let cpu_family = match std::env::consts::ARCH { + "blackfin" => CpuFamily::CPU_BLACKFIN, + "arm" | "arm64" => CpuFamily::CPU_ARM, + "ia64" => CpuFamily::CPU_IA64, + "mips" => CpuFamily::CPU_MIPS, + "ppc" => CpuFamily::CPU_PPC, + "ppc64" => CpuFamily::CPU_PPC_64, + "sh" => CpuFamily::CPU_SH, + "x86" => CpuFamily::CPU_X86, + "x86_64" => CpuFamily::CPU_X86_64, + _ => CpuFamily::CPU_UNKNOWN, + }; + + let os = match std::env::consts::OS { + "android" => Os::OS_ANDROID, + "freebsd" | "netbsd" | "openbsd" => Os::OS_FREEBSD, + "ios" => Os::OS_IPHONE, + "linux" => Os::OS_LINUX, + "macos" => Os::OS_OSX, + "windows" => Os::OS_WINDOWS, + _ => Os::OS_UNKNOWN, + }; + let mut packet = ClientResponseEncrypted::new(); packet .mut_login_credentials() @@ -81,21 +104,19 @@ pub async fn authenticate( packet .mut_login_credentials() .set_auth_data(credentials.auth_data); - packet - .mut_system_info() - .set_cpu_family(CpuFamily::CPU_UNKNOWN); - packet.mut_system_info().set_os(Os::OS_UNKNOWN); + packet.mut_system_info().set_cpu_family(cpu_family); + packet.mut_system_info().set_os(os); packet .mut_system_info() .set_system_information_string(format!( - "librespot_{}_{}", + "librespot-{}-{}", version::SHA_SHORT, version::BUILD_ID )); packet .mut_system_info() .set_device_id(device_id.to_string()); - packet.set_version_string(version::VERSION_STRING.to_string()); + packet.set_version_string(format!("librespot {}", version::SEMVER)); let cmd = PacketType::Login; let data = packet.write_to_bytes().unwrap(); diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 21a6c0a6..157fbaef 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -5,9 +5,12 @@ use hyper::header::InvalidHeaderValue; use hyper::{Body, Client, Request, Response, StatusCode}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::HttpsConnector; +use std::env::consts::OS; use thiserror::Error; use url::Url; +use crate::version; + pub struct HttpClient { proxy: Option, } @@ -50,11 +53,29 @@ impl HttpClient { let connector = HttpsConnector::with_native_roots(); + let spotify_version = match OS { + "android" | "ios" => "8.6.84", + _ => "117300517", + }; + + let spotify_platform = match OS { + "android" => "Android/31", + "ios" => "iOS/15.1.1", + "macos" => "OSX/0", + "windows" => "Win32/0", + _ => "Linux/0", + }; + let headers_mut = req.headers_mut(); headers_mut.insert( "User-Agent", // Some features like lyrics are version-gated and require an official version string. - HeaderValue::from_str("Spotify/8.6.80 iOS/13.5 (iPhone11,2)")?, + HeaderValue::from_str(&format!( + "Spotify/{} {} ({})", + spotify_version, + spotify_platform, + version::VERSION_STRING + ))?, ); let response = if let Some(url) = &self.proxy { diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 57f5bf46..a82f90c0 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -57,7 +57,7 @@ impl RequestHandler { "status": 101, "statusString": "ERROR-OK", "spotifyError": 0, - "version": "2.7.1", + "version": crate::core::version::SEMVER, "deviceID": (self.config.device_id), "remoteName": (self.config.name), "activeUser": "", From 40163754bbffd554746aceb5944f2b12fc27e914 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 10 Dec 2021 20:33:43 +0100 Subject: [PATCH 64/95] Update protobufs to 1.1.73.517 --- metadata/src/episode.rs | 5 + metadata/src/playlist/item.rs | 6 + metadata/src/playlist/list.rs | 28 +++- metadata/src/playlist/mod.rs | 1 + metadata/src/playlist/permission.rs | 44 +++++ metadata/src/show.rs | 2 + protocol/build.rs | 2 + protocol/proto/AdContext.proto | 19 +++ protocol/proto/AdEvent.proto | 3 +- protocol/proto/CacheError.proto | 5 +- protocol/proto/CacheReport.proto | 4 +- protocol/proto/ConnectionStateChange.proto | 13 ++ protocol/proto/DesktopDeviceInformation.proto | 106 ++++++++++++ protocol/proto/DesktopPerformanceIssue.proto | 88 ++++++++++ protocol/proto/Download.proto | 4 +- protocol/proto/EventSenderStats2NonAuth.proto | 23 +++ protocol/proto/HeadFileDownload.proto | 3 +- protocol/proto/LegacyEndSong.proto | 62 +++++++ protocol/proto/LocalFilesError.proto | 3 +- protocol/proto/LocalFilesImport.proto | 3 +- protocol/proto/MercuryCacheReport.proto | 20 --- protocol/proto/ModuleDebug.proto | 11 -- .../proto/OfflineUserPwdLoginNonAuth.proto | 11 -- protocol/proto/RawCoreStream.proto | 52 ++++++ protocol/proto/anchor_extended_metadata.proto | 14 -- protocol/proto/apiv1.proto | 7 +- protocol/proto/app_state.proto | 17 ++ .../proto/autodownload_backend_service.proto | 53 ++++++ .../proto/autodownload_config_common.proto | 19 +++ .../autodownload_config_get_request.proto | 22 +++ .../autodownload_config_set_request.proto | 23 +++ protocol/proto/automix_mode.proto | 22 ++- protocol/proto/canvas_storage.proto | 19 +++ protocol/proto/canvaz-meta.proto | 9 +- protocol/proto/canvaz.proto | 14 +- protocol/proto/client-tts.proto | 30 ++++ protocol/proto/client_config.proto | 13 ++ protocol/proto/cloud_host_messages.proto | 152 ------------------ .../collection/episode_collection_state.proto | 3 +- .../collection_add_remove_items_request.proto | 17 ++ protocol/proto/collection_ban_request.proto | 19 +++ .../proto/collection_decoration_policy.proto | 38 +++++ .../proto/collection_get_bans_request.proto | 33 ++++ protocol/proto/collection_index.proto | 22 ++- protocol/proto/collection_item.proto | 48 ++++++ .../proto/collection_platform_requests.proto | 7 +- .../proto/collection_platform_responses.proto | 9 +- protocol/proto/collection_storage.proto | 20 --- protocol/proto/composite_formats_node.proto | 31 ---- protocol/proto/connect.proto | 8 +- .../proto/context_application_desktop.proto | 12 ++ protocol/proto/context_core.proto | 14 -- protocol/proto/context_device_desktop.proto | 15 ++ protocol/proto/context_node.proto | 3 +- protocol/proto/context_player_ng.proto | 12 -- protocol/proto/context_sdk.proto | 3 +- .../core_configuration_applied_non_auth.proto | 11 -- protocol/proto/cosmos_changes_request.proto | 3 +- protocol/proto/cosmos_decorate_request.proto | 3 +- .../proto/cosmos_get_album_list_request.proto | 3 +- .../cosmos_get_artist_list_request.proto | 3 +- .../cosmos_get_episode_list_request.proto | 3 +- .../proto/cosmos_get_show_list_request.proto | 3 +- .../proto/cosmos_get_tags_info_request.proto | 3 +- ...smos_get_track_list_metadata_request.proto | 3 +- .../proto/cosmos_get_track_list_request.proto | 3 +- ...cosmos_get_unplayed_episodes_request.proto | 3 +- protocol/proto/decorate_request.proto | 10 +- .../proto/dependencies/session_control.proto | 121 -------------- .../proto/display_segments_extension.proto | 54 +++++++ protocol/proto/es_command_options.proto | 3 +- protocol/proto/es_ident.proto | 11 ++ protocol/proto/es_ident_filter.proto | 11 ++ protocol/proto/es_prefs.proto | 53 ++++++ protocol/proto/es_pushed_message.proto | 15 ++ protocol/proto/es_remote_config.proto | 21 +++ protocol/proto/es_request_info.proto | 27 ++++ protocol/proto/es_seek_to.proto | 9 +- protocol/proto/es_storage.proto | 88 ++++++++++ protocol/proto/event_entity.proto | 8 +- .../proto/extension_descriptor_type.proto | 3 +- protocol/proto/extension_kind.proto | 10 +- protocol/proto/follow_request.proto | 21 +++ protocol/proto/followed_users_request.proto | 21 +++ .../proto/google/protobuf/descriptor.proto | 4 +- protocol/proto/google/protobuf/empty.proto | 17 ++ protocol/proto/greenroom_extension.proto | 29 ++++ .../{format.proto => media_format.proto} | 6 +- protocol/proto/media_manifest.proto | 13 +- protocol/proto/media_type.proto | 7 +- protocol/proto/members_request.proto | 18 +++ protocol/proto/members_response.proto | 35 ++++ .../messages/discovery/force_discover.proto | 15 ++ .../messages/discovery/start_discovery.proto | 15 ++ protocol/proto/metadata.proto | 5 +- .../proto/metadata/episode_metadata.proto | 6 +- protocol/proto/metadata/extension.proto | 16 ++ protocol/proto/metadata/show_metadata.proto | 5 +- protocol/proto/metadata_esperanto.proto | 24 +++ protocol/proto/mod.rs | 2 - .../proto/offline_playlists_containing.proto | 3 +- .../proto/on_demand_set_cosmos_request.proto | 5 +- .../proto/on_demand_set_cosmos_response.proto | 5 +- protocol/proto/on_demand_set_response.proto | 15 ++ protocol/proto/pending_event_entity.proto | 13 ++ protocol/proto/perf_metrics_service.proto | 20 +++ protocol/proto/pin_request.proto | 3 +- protocol/proto/play_reason.proto | 45 +++--- protocol/proto/play_source.proto | 47 ------ protocol/proto/playback_cosmos.proto | 5 +- protocol/proto/playback_esperanto.proto | 122 ++++++++++++++ protocol/proto/playback_platform.proto | 90 +++++++++++ .../played_state/show_played_state.proto | 3 +- protocol/proto/playlist4_external.proto | 63 +++++++- .../proto/playlist_contains_request.proto | 23 +++ protocol/proto/playlist_members_request.proto | 19 +++ protocol/proto/playlist_offline_request.proto | 29 ++++ protocol/proto/playlist_permission.proto | 22 ++- protocol/proto/playlist_playlist_state.proto | 4 +- protocol/proto/playlist_request.proto | 4 +- ...aylist_set_member_permission_request.proto | 16 ++ protocol/proto/playlist_track_state.proto | 3 +- protocol/proto/playlist_user_state.proto | 3 +- protocol/proto/playlist_v1_uri.proto | 15 -- protocol/proto/podcast_cta_cards.proto | 9 ++ protocol/proto/podcast_ratings.proto | 32 ++++ .../policy/album_decoration_policy.proto | 14 +- .../policy/artist_decoration_policy.proto | 17 +- .../policy/episode_decoration_policy.proto | 8 +- .../policy/playlist_decoration_policy.proto | 5 +- .../proto/policy/show_decoration_policy.proto | 6 +- .../policy/track_decoration_policy.proto | 14 +- .../proto/policy/user_decoration_policy.proto | 3 +- protocol/proto/prepare_play_options.proto | 23 ++- protocol/proto/profile_cache.proto | 19 --- protocol/proto/profile_service.proto | 33 ++++ protocol/proto/property_definition.proto | 2 +- protocol/proto/rate_limited_events.proto | 12 ++ .../proto/rc_dummy_property_resolved.proto | 12 -- protocol/proto/rcs.proto | 2 +- protocol/proto/record_id.proto | 4 +- protocol/proto/resolve.proto | 2 +- .../proto/resolve_configuration_error.proto | 14 -- protocol/proto/response_status.proto | 4 +- protocol/proto/rootlist_request.proto | 5 +- protocol/proto/sequence_number_entity.proto | 6 +- .../proto/set_member_permission_request.proto | 18 +++ protocol/proto/show_access.proto | 19 ++- protocol/proto/show_episode_state.proto | 9 +- protocol/proto/show_request.proto | 17 +- protocol/proto/show_show_state.proto | 3 +- protocol/proto/social_connect_v2.proto | 24 ++- protocol/proto/social_service.proto | 52 ++++++ .../proto/socialgraph_response_status.proto | 15 ++ protocol/proto/socialgraphv2.proto | 45 ++++++ .../ads_rules_inject_tracks.proto | 14 ++ .../behavior_metadata_rules.proto | 12 ++ .../state_restore/circuit_breaker_rules.proto | 13 ++ .../state_restore/context_player_rules.proto | 16 ++ .../context_player_rules_base.proto | 33 ++++ .../explicit_content_rules.proto | 12 ++ .../explicit_request_rules.proto | 11 ++ .../state_restore/mft_context_history.proto | 19 +++ .../mft_context_switch_rules.proto | 10 ++ .../mft_fallback_page_history.proto | 16 ++ protocol/proto/state_restore/mft_rules.proto | 38 +++++ .../proto/state_restore/mft_rules_core.proto | 16 ++ .../mft_rules_inject_filler_tracks.proto | 23 +++ protocol/proto/state_restore/mft_state.proto | 31 ++++ .../mod_interruption_state.proto | 23 +++ .../mod_rules_interruptions.proto | 27 ++++ .../state_restore/music_injection_rules.proto | 25 +++ .../state_restore/player_session_queue.proto | 27 ++++ .../proto/state_restore/provided_track.proto | 20 +++ .../proto/state_restore/random_source.proto | 12 ++ .../remove_banned_tracks_rules.proto | 18 +++ .../state_restore/resume_points_rules.proto | 17 ++ .../state_restore/track_error_rules.proto | 13 ++ protocol/proto/status.proto | 12 ++ protocol/proto/status_code.proto | 4 +- protocol/proto/stream_end_request.proto | 7 +- protocol/proto/stream_prepare_request.proto | 39 ----- protocol/proto/stream_seek_request.proto | 4 +- protocol/proto/stream_start_request.proto | 40 ++++- ...onse.proto => stream_start_response.proto} | 10 +- protocol/proto/streaming_rule.proto | 13 +- protocol/proto/sync_request.proto | 3 +- protocol/proto/test_request_failure.proto | 14 -- .../track_offlining_cosmos_response.proto | 24 --- protocol/proto/tts-resolve.proto | 6 +- .../proto/unfinished_episodes_request.proto | 6 +- .../proto/your_library_contains_request.proto | 5 +- .../proto/your_library_decorate_request.proto | 9 +- .../your_library_decorate_response.proto | 6 +- .../proto/your_library_decorated_entity.proto | 105 ++++++++++++ protocol/proto/your_library_entity.proto | 19 ++- protocol/proto/your_library_index.proto | 21 +-- .../your_library_pseudo_playlist_config.proto | 19 +++ protocol/proto/your_library_request.proto | 64 +------- protocol/proto/your_library_response.proto | 103 +----------- 200 files changed, 3016 insertions(+), 978 deletions(-) create mode 100644 metadata/src/playlist/permission.rs create mode 100644 protocol/proto/AdContext.proto create mode 100644 protocol/proto/ConnectionStateChange.proto create mode 100644 protocol/proto/DesktopDeviceInformation.proto create mode 100644 protocol/proto/DesktopPerformanceIssue.proto create mode 100644 protocol/proto/EventSenderStats2NonAuth.proto create mode 100644 protocol/proto/LegacyEndSong.proto delete mode 100644 protocol/proto/MercuryCacheReport.proto delete mode 100644 protocol/proto/ModuleDebug.proto delete mode 100644 protocol/proto/OfflineUserPwdLoginNonAuth.proto create mode 100644 protocol/proto/RawCoreStream.proto delete mode 100644 protocol/proto/anchor_extended_metadata.proto create mode 100644 protocol/proto/app_state.proto create mode 100644 protocol/proto/autodownload_backend_service.proto create mode 100644 protocol/proto/autodownload_config_common.proto create mode 100644 protocol/proto/autodownload_config_get_request.proto create mode 100644 protocol/proto/autodownload_config_set_request.proto create mode 100644 protocol/proto/canvas_storage.proto create mode 100644 protocol/proto/client-tts.proto create mode 100644 protocol/proto/client_config.proto delete mode 100644 protocol/proto/cloud_host_messages.proto create mode 100644 protocol/proto/collection_add_remove_items_request.proto create mode 100644 protocol/proto/collection_ban_request.proto create mode 100644 protocol/proto/collection_decoration_policy.proto create mode 100644 protocol/proto/collection_get_bans_request.proto create mode 100644 protocol/proto/collection_item.proto delete mode 100644 protocol/proto/collection_storage.proto delete mode 100644 protocol/proto/composite_formats_node.proto create mode 100644 protocol/proto/context_application_desktop.proto delete mode 100644 protocol/proto/context_core.proto create mode 100644 protocol/proto/context_device_desktop.proto delete mode 100644 protocol/proto/context_player_ng.proto delete mode 100644 protocol/proto/core_configuration_applied_non_auth.proto delete mode 100644 protocol/proto/dependencies/session_control.proto create mode 100644 protocol/proto/display_segments_extension.proto create mode 100644 protocol/proto/es_ident.proto create mode 100644 protocol/proto/es_ident_filter.proto create mode 100644 protocol/proto/es_prefs.proto create mode 100644 protocol/proto/es_pushed_message.proto create mode 100644 protocol/proto/es_remote_config.proto create mode 100644 protocol/proto/es_request_info.proto create mode 100644 protocol/proto/es_storage.proto create mode 100644 protocol/proto/follow_request.proto create mode 100644 protocol/proto/followed_users_request.proto create mode 100644 protocol/proto/google/protobuf/empty.proto create mode 100644 protocol/proto/greenroom_extension.proto rename protocol/proto/{format.proto => media_format.proto} (84%) create mode 100644 protocol/proto/members_request.proto create mode 100644 protocol/proto/members_response.proto create mode 100644 protocol/proto/messages/discovery/force_discover.proto create mode 100644 protocol/proto/messages/discovery/start_discovery.proto create mode 100644 protocol/proto/metadata/extension.proto create mode 100644 protocol/proto/metadata_esperanto.proto create mode 100644 protocol/proto/on_demand_set_response.proto create mode 100644 protocol/proto/pending_event_entity.proto create mode 100644 protocol/proto/perf_metrics_service.proto delete mode 100644 protocol/proto/play_source.proto create mode 100644 protocol/proto/playback_esperanto.proto create mode 100644 protocol/proto/playback_platform.proto create mode 100644 protocol/proto/playlist_contains_request.proto create mode 100644 protocol/proto/playlist_members_request.proto create mode 100644 protocol/proto/playlist_offline_request.proto create mode 100644 protocol/proto/playlist_set_member_permission_request.proto delete mode 100644 protocol/proto/playlist_v1_uri.proto create mode 100644 protocol/proto/podcast_cta_cards.proto create mode 100644 protocol/proto/podcast_ratings.proto delete mode 100644 protocol/proto/profile_cache.proto create mode 100644 protocol/proto/profile_service.proto create mode 100644 protocol/proto/rate_limited_events.proto delete mode 100644 protocol/proto/rc_dummy_property_resolved.proto delete mode 100644 protocol/proto/resolve_configuration_error.proto create mode 100644 protocol/proto/set_member_permission_request.proto create mode 100644 protocol/proto/social_service.proto create mode 100644 protocol/proto/socialgraph_response_status.proto create mode 100644 protocol/proto/socialgraphv2.proto create mode 100644 protocol/proto/state_restore/ads_rules_inject_tracks.proto create mode 100644 protocol/proto/state_restore/behavior_metadata_rules.proto create mode 100644 protocol/proto/state_restore/circuit_breaker_rules.proto create mode 100644 protocol/proto/state_restore/context_player_rules.proto create mode 100644 protocol/proto/state_restore/context_player_rules_base.proto create mode 100644 protocol/proto/state_restore/explicit_content_rules.proto create mode 100644 protocol/proto/state_restore/explicit_request_rules.proto create mode 100644 protocol/proto/state_restore/mft_context_history.proto create mode 100644 protocol/proto/state_restore/mft_context_switch_rules.proto create mode 100644 protocol/proto/state_restore/mft_fallback_page_history.proto create mode 100644 protocol/proto/state_restore/mft_rules.proto create mode 100644 protocol/proto/state_restore/mft_rules_core.proto create mode 100644 protocol/proto/state_restore/mft_rules_inject_filler_tracks.proto create mode 100644 protocol/proto/state_restore/mft_state.proto create mode 100644 protocol/proto/state_restore/mod_interruption_state.proto create mode 100644 protocol/proto/state_restore/mod_rules_interruptions.proto create mode 100644 protocol/proto/state_restore/music_injection_rules.proto create mode 100644 protocol/proto/state_restore/player_session_queue.proto create mode 100644 protocol/proto/state_restore/provided_track.proto create mode 100644 protocol/proto/state_restore/random_source.proto create mode 100644 protocol/proto/state_restore/remove_banned_tracks_rules.proto create mode 100644 protocol/proto/state_restore/resume_points_rules.proto create mode 100644 protocol/proto/state_restore/track_error_rules.proto create mode 100644 protocol/proto/status.proto delete mode 100644 protocol/proto/stream_prepare_request.proto rename protocol/proto/{stream_prepare_response.proto => stream_start_response.proto} (57%) delete mode 100644 protocol/proto/test_request_failure.proto delete mode 100644 protocol/proto/track_offlining_cosmos_response.proto create mode 100644 protocol/proto/your_library_decorated_entity.proto create mode 100644 protocol/proto/your_library_pseudo_playlist_config.proto diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 35d6ed8f..30c89f19 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -8,6 +8,7 @@ use crate::{ item::{AudioItem, AudioItemResult, InnerAudioItem}, }, availability::Availabilities, + content_rating::ContentRatings, date::Date, error::{MetadataError, RequestError}, image::Images, @@ -48,6 +49,8 @@ pub struct Episode { pub external_url: String, pub episode_type: EpisodeType, pub has_music_and_talk: bool, + pub content_rating: ContentRatings, + pub is_audiobook_chapter: bool, } #[derive(Debug, Clone)] @@ -125,6 +128,8 @@ impl TryFrom<&::Message> for Episode { external_url: episode.get_external_url().to_owned(), episode_type: episode.get_field_type(), has_music_and_talk: episode.get_music_and_talk(), + content_rating: episode.get_content_rating().into(), + is_audiobook_chapter: episode.get_is_audiobook_chapter(), }) } } diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index 975a9840..de2dc6db 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -9,6 +9,8 @@ use super::attribute::{PlaylistAttributes, PlaylistItemAttributes}; use librespot_core::spotify_id::SpotifyId; use librespot_protocol as protocol; +use super::permission::Capabilities; + use protocol::playlist4_external::Item as PlaylistItemMessage; use protocol::playlist4_external::ListItems as PlaylistItemsMessage; use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage; @@ -44,6 +46,8 @@ pub struct PlaylistMetaItem { pub length: i32, pub timestamp: Date, pub owner_username: String, + pub has_abuse_reporting: bool, + pub capabilities: Capabilities, } #[derive(Debug, Clone)] @@ -89,6 +93,8 @@ impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem { length: item.get_length(), timestamp: item.get_timestamp().try_into()?, owner_username: item.get_owner_username().to_owned(), + has_abuse_reporting: item.get_abuse_reporting_enabled(), + capabilities: item.get_capabilities().into(), }) } } diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 7b5f0121..625373db 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -8,16 +8,31 @@ use crate::{ date::Date, error::MetadataError, request::{MercuryRequest, RequestResult}, - util::try_from_repeated_message, + util::{from_repeated_enum, try_from_repeated_message}, Metadata, }; -use super::{attribute::PlaylistAttributes, diff::PlaylistDiff, item::PlaylistItemList}; +use super::{ + attribute::PlaylistAttributes, diff::PlaylistDiff, item::PlaylistItemList, + permission::Capabilities, +}; use librespot_core::session::Session; use librespot_core::spotify_id::{NamedSpotifyId, SpotifyId}; use librespot_protocol as protocol; +use protocol::playlist4_external::GeoblockBlockingType as Geoblock; + +#[derive(Debug, Clone)] +pub struct Geoblocks(Vec); + +impl Deref for Geoblocks { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Debug, Clone)] pub struct Playlist { pub id: NamedSpotifyId, @@ -33,6 +48,8 @@ pub struct Playlist { pub nonces: Vec, pub timestamp: Date, pub has_abuse_reporting: bool, + pub capabilities: Capabilities, + pub geoblocks: Geoblocks, } #[derive(Debug, Clone)] @@ -70,6 +87,8 @@ pub struct SelectedListContent { pub timestamp: Date, pub owner_username: String, pub has_abuse_reporting: bool, + pub capabilities: Capabilities, + pub geoblocks: Geoblocks, } impl Playlist { @@ -153,6 +172,8 @@ impl Metadata for Playlist { nonces: playlist.nonces, timestamp: playlist.timestamp, has_abuse_reporting: playlist.has_abuse_reporting, + capabilities: playlist.capabilities, + geoblocks: playlist.geoblocks, }) } } @@ -194,8 +215,11 @@ impl TryFrom<&::Message> for SelectedListContent { timestamp: playlist.get_timestamp().try_into()?, owner_username: playlist.get_owner_username().to_owned(), has_abuse_reporting: playlist.get_abuse_reporting_enabled(), + capabilities: playlist.get_capabilities().into(), + geoblocks: playlist.get_geoblock().into(), }) } } +from_repeated_enum!(Geoblock, Geoblocks); try_from_repeated_message!(Vec, Playlists); diff --git a/metadata/src/playlist/mod.rs b/metadata/src/playlist/mod.rs index c52e637b..d2b66731 100644 --- a/metadata/src/playlist/mod.rs +++ b/metadata/src/playlist/mod.rs @@ -4,6 +4,7 @@ pub mod diff; pub mod item; pub mod list; pub mod operation; +pub mod permission; pub use annotation::PlaylistAnnotation; pub use list::Playlist; diff --git a/metadata/src/playlist/permission.rs b/metadata/src/playlist/permission.rs new file mode 100644 index 00000000..163859a1 --- /dev/null +++ b/metadata/src/playlist/permission.rs @@ -0,0 +1,44 @@ +use std::fmt::Debug; +use std::ops::Deref; + +use crate::util::from_repeated_enum; + +use librespot_protocol as protocol; + +use protocol::playlist_permission::Capabilities as CapabilitiesMessage; +use protocol::playlist_permission::PermissionLevel; + +#[derive(Debug, Clone)] +pub struct Capabilities { + pub can_view: bool, + pub can_administrate_permissions: bool, + pub grantable_levels: PermissionLevels, + pub can_edit_metadata: bool, + pub can_edit_items: bool, + pub can_cancel_membership: bool, +} + +#[derive(Debug, Clone)] +pub struct PermissionLevels(pub Vec); + +impl Deref for PermissionLevels { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&CapabilitiesMessage> for Capabilities { + fn from(playlist: &CapabilitiesMessage) -> Self { + Self { + can_view: playlist.get_can_view(), + can_administrate_permissions: playlist.get_can_administrate_permissions(), + grantable_levels: playlist.get_grantable_level().into(), + can_edit_metadata: playlist.get_can_edit_metadata(), + can_edit_items: playlist.get_can_edit_items(), + can_cancel_membership: playlist.get_can_cancel_membership(), + } + } +} + +from_repeated_enum!(PermissionLevel, PermissionLevels); diff --git a/metadata/src/show.rs b/metadata/src/show.rs index 4e75c598..f69ee021 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -31,6 +31,7 @@ pub struct Show { pub availability: Availabilities, pub trailer_uri: SpotifyId, pub has_music_and_talk: bool, + pub is_audiobook: bool, } #[async_trait] @@ -70,6 +71,7 @@ impl TryFrom<&::Message> for Show { availability: show.get_availability().into(), trailer_uri: SpotifyId::from_uri(show.get_trailer_uri())?, has_music_and_talk: show.get_music_and_talk(), + is_audiobook: show.get_is_audiobook(), }) } } diff --git a/protocol/build.rs b/protocol/build.rs index 560bbfea..2a763183 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -24,11 +24,13 @@ fn compile() { proto_dir.join("metadata.proto"), proto_dir.join("player.proto"), proto_dir.join("playlist_annotate3.proto"), + proto_dir.join("playlist_permission.proto"), proto_dir.join("playlist4_external.proto"), // TODO: remove these legacy protobufs when we are on the new API completely proto_dir.join("authentication.proto"), proto_dir.join("canvaz.proto"), proto_dir.join("canvaz-meta.proto"), + proto_dir.join("explicit_content_pubsub.proto"), proto_dir.join("keyexchange.proto"), proto_dir.join("mercury.proto"), proto_dir.join("pubsub.proto"), diff --git a/protocol/proto/AdContext.proto b/protocol/proto/AdContext.proto new file mode 100644 index 00000000..ba56bd00 --- /dev/null +++ b/protocol/proto/AdContext.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AdContext { + optional string preceding_content_uri = 1; + optional string preceding_playback_id = 2; + optional int32 preceding_end_position = 3; + repeated string ad_ids = 4; + optional string ad_request_id = 5; + optional string succeeding_content_uri = 6; + optional string succeeding_playback_id = 7; + optional int32 succeeding_start_position = 8; + optional int32 preceding_duration = 9; +} diff --git a/protocol/proto/AdEvent.proto b/protocol/proto/AdEvent.proto index 4b0a3059..69cf82bb 100644 --- a/protocol/proto/AdEvent.proto +++ b/protocol/proto/AdEvent.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -24,4 +24,5 @@ message AdEvent { optional int32 duration = 15; optional bool in_focus = 16; optional float volume = 17; + optional string product_name = 18; } diff --git a/protocol/proto/CacheError.proto b/protocol/proto/CacheError.proto index 8da6196d..ad85c342 100644 --- a/protocol/proto/CacheError.proto +++ b/protocol/proto/CacheError.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -13,4 +13,7 @@ message CacheError { optional bytes file_id = 4; optional int64 num_errors = 5; optional string cache_path = 6; + optional int64 size = 7; + optional int64 range_start = 8; + optional int64 range_end = 9; } diff --git a/protocol/proto/CacheReport.proto b/protocol/proto/CacheReport.proto index c8666ca3..ac034059 100644 --- a/protocol/proto/CacheReport.proto +++ b/protocol/proto/CacheReport.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -8,6 +8,8 @@ option optimize_for = CODE_SIZE; message CacheReport { optional bytes cache_id = 1; + optional string cache_path = 21; + optional string volatile_path = 22; optional int64 max_cache_size = 2; optional int64 free_space = 3; optional int64 total_space = 4; diff --git a/protocol/proto/ConnectionStateChange.proto b/protocol/proto/ConnectionStateChange.proto new file mode 100644 index 00000000..28e517c0 --- /dev/null +++ b/protocol/proto/ConnectionStateChange.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConnectionStateChange { + optional string type = 1; + optional string old = 2; + optional string new = 3; +} diff --git a/protocol/proto/DesktopDeviceInformation.proto b/protocol/proto/DesktopDeviceInformation.proto new file mode 100644 index 00000000..be503177 --- /dev/null +++ b/protocol/proto/DesktopDeviceInformation.proto @@ -0,0 +1,106 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopDeviceInformation { + optional string os_platform = 1; + optional string os_version = 2; + optional string computer_manufacturer = 3; + optional string mac_computer_model = 4; + optional string mac_computer_model_family = 5; + optional bool computer_has_internal_battery = 6; + optional bool computer_is_currently_running_on_battery_power = 7; + optional string mac_cpu_product_name = 8; + optional int64 mac_cpu_family_code = 9; + optional int64 cpu_num_physical_cores = 10; + optional int64 cpu_num_logical_cores = 11; + optional int64 cpu_clock_frequency_herz = 12; + optional int64 cpu_level_1_cache_size_bytes = 13; + optional int64 cpu_level_2_cache_size_bytes = 14; + optional int64 cpu_level_3_cache_size_bytes = 15; + optional bool cpu_is_64_bit_capable = 16; + optional int64 computer_ram_size_bytes = 17; + optional int64 computer_ram_speed_herz = 18; + optional int64 num_graphics_cards = 19; + optional int64 num_connected_screens = 20; + optional string app_screen_model_name = 21; + optional double app_screen_width_logical_points = 22; + optional double app_screen_height_logical_points = 23; + optional double mac_app_screen_scale_factor = 24; + optional double app_screen_physical_size_inches = 25; + optional int64 app_screen_bits_per_pixel = 26; + optional bool app_screen_supports_dci_p3_color_gamut = 27; + optional bool app_screen_is_built_in = 28; + optional string app_screen_graphics_card_model = 29; + optional int64 app_screen_graphics_card_vram_size_bytes = 30; + optional bool mac_app_screen_currently_contains_the_dock = 31; + optional bool mac_app_screen_currently_contains_active_menu_bar = 32; + optional bool boot_disk_is_known_ssd = 33; + optional string mac_boot_disk_connection_type = 34; + optional int64 boot_disk_capacity_bytes = 35; + optional int64 boot_disk_free_space_bytes = 36; + optional bool application_disk_is_same_as_boot_disk = 37; + optional bool application_disk_is_known_ssd = 38; + optional string mac_application_disk_connection_type = 39; + optional int64 application_disk_capacity_bytes = 40; + optional int64 application_disk_free_space_bytes = 41; + optional bool application_cache_disk_is_same_as_boot_disk = 42; + optional bool application_cache_disk_is_known_ssd = 43; + optional string mac_application_cache_disk_connection_type = 44; + optional int64 application_cache_disk_capacity_bytes = 45; + optional int64 application_cache_disk_free_space_bytes = 46; + optional bool has_pointing_device = 47; + optional bool has_builtin_pointing_device = 48; + optional bool has_touchpad = 49; + optional bool has_keyboard = 50; + optional bool has_builtin_keyboard = 51; + optional bool mac_has_touch_bar = 52; + optional bool has_touch_screen = 53; + optional bool has_pen_input = 54; + optional bool has_game_controller = 55; + optional bool has_bluetooth_support = 56; + optional int64 bluetooth_link_manager_version = 57; + optional string bluetooth_version_string = 58; + optional int64 num_audio_output_devices = 59; + optional string default_audio_output_device_name = 60; + optional string default_audio_output_device_manufacturer = 61; + optional double default_audio_output_device_current_sample_rate = 62; + optional int64 default_audio_output_device_current_bit_depth = 63; + optional int64 default_audio_output_device_current_buffer_size = 64; + optional int64 default_audio_output_device_current_num_channels = 65; + optional double default_audio_output_device_maximum_sample_rate = 66; + optional int64 default_audio_output_device_maximum_bit_depth = 67; + optional int64 default_audio_output_device_maximum_num_channels = 68; + optional bool default_audio_output_device_is_builtin = 69; + optional bool default_audio_output_device_is_virtual = 70; + optional string mac_default_audio_output_device_transport_type = 71; + optional string mac_default_audio_output_device_terminal_type = 72; + optional int64 num_video_capture_devices = 73; + optional string default_video_capture_device_manufacturer = 74; + optional string default_video_capture_device_model = 75; + optional string default_video_capture_device_name = 76; + optional int64 default_video_capture_device_image_width = 77; + optional int64 default_video_capture_device_image_height = 78; + optional string mac_default_video_capture_device_transport_type = 79; + optional bool default_video_capture_device_is_builtin = 80; + optional int64 num_active_network_interfaces = 81; + optional string mac_main_network_interface_name = 82; + optional string mac_main_network_interface_type = 83; + optional bool main_network_interface_supports_ipv4 = 84; + optional bool main_network_interface_supports_ipv6 = 85; + optional string main_network_interface_hardware_vendor = 86; + optional string main_network_interface_hardware_model = 87; + optional int64 main_network_interface_medium_speed_bps = 88; + optional int64 main_network_interface_link_speed_bps = 89; + optional double system_up_time_including_sleep_seconds = 90; + optional double system_up_time_awake_seconds = 91; + optional double app_up_time_including_sleep_seconds = 92; + optional string system_user_preferred_language_code = 93; + optional string system_user_preferred_locale = 94; + optional string mac_app_system_localization_language = 95; + optional string app_localization_language = 96; +} diff --git a/protocol/proto/DesktopPerformanceIssue.proto b/protocol/proto/DesktopPerformanceIssue.proto new file mode 100644 index 00000000..4e70b435 --- /dev/null +++ b/protocol/proto/DesktopPerformanceIssue.proto @@ -0,0 +1,88 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopPerformanceIssue { + optional string event_type = 1; + optional bool is_continuation_event = 2; + optional double sample_time_interval_seconds = 3; + optional string computer_platform = 4; + optional double last_seen_main_thread_latency_seconds = 5; + optional double last_seen_core_thread_latency_seconds = 6; + optional double total_spotify_processes_cpu_load_percent = 7; + optional double main_process_cpu_load_percent = 8; + optional int64 mac_main_process_vm_size_bytes = 9; + optional int64 mac_main_process_resident_size_bytes = 10; + optional double mac_main_process_num_page_faults_per_second = 11; + optional double mac_main_process_num_pageins_per_second = 12; + optional double mac_main_process_num_cow_faults_per_second = 13; + optional double mac_main_process_num_context_switches_per_second = 14; + optional int64 main_process_num_total_threads = 15; + optional int64 main_process_num_running_threads = 16; + optional double renderer_process_cpu_load_percent = 17; + optional int64 mac_renderer_process_vm_size_bytes = 18; + optional int64 mac_renderer_process_resident_size_bytes = 19; + optional double mac_renderer_process_num_page_faults_per_second = 20; + optional double mac_renderer_process_num_pageins_per_second = 21; + optional double mac_renderer_process_num_cow_faults_per_second = 22; + optional double mac_renderer_process_num_context_switches_per_second = 23; + optional int64 renderer_process_num_total_threads = 24; + optional int64 renderer_process_num_running_threads = 25; + optional double system_total_cpu_load_percent = 26; + optional int64 mac_system_total_free_memory_size_bytes = 27; + optional int64 mac_system_total_active_memory_size_bytes = 28; + optional int64 mac_system_total_inactive_memory_size_bytes = 29; + optional int64 mac_system_total_wired_memory_size_bytes = 30; + optional int64 mac_system_total_compressed_memory_size_bytes = 31; + optional double mac_system_current_num_pageins_per_second = 32; + optional double mac_system_current_num_pageouts_per_second = 33; + optional double mac_system_current_num_page_faults_per_second = 34; + optional double mac_system_current_num_cow_faults_per_second = 35; + optional int64 system_current_num_total_processes = 36; + optional int64 system_current_num_total_threads = 37; + optional int64 computer_boot_disk_free_space_bytes = 38; + optional int64 application_disk_free_space_bytes = 39; + optional int64 application_cache_disk_free_space_bytes = 40; + optional bool computer_is_currently_running_on_battery_power = 41; + optional double computer_remaining_battery_capacity_percent = 42; + optional double computer_estimated_remaining_battery_time_seconds = 43; + optional int64 mac_computer_num_available_logical_cpu_cores_due_to_power_management = 44; + optional double mac_computer_current_processor_speed_percent_due_to_power_management = 45; + optional double mac_computer_current_cpu_time_limit_percent_due_to_power_management = 46; + optional double app_screen_width_points = 47; + optional double app_screen_height_points = 48; + optional double mac_app_screen_scale_factor = 49; + optional int64 app_screen_bits_per_pixel = 50; + optional bool app_screen_supports_dci_p3_color_gamut = 51; + optional bool app_screen_is_built_in = 52; + optional string app_screen_graphics_card_model = 53; + optional int64 app_screen_graphics_card_vram_size_bytes = 54; + optional double app_window_width_points = 55; + optional double app_window_height_points = 56; + optional double app_window_percentage_on_screen = 57; + optional double app_window_percentage_non_obscured = 58; + optional double system_up_time_including_sleep_seconds = 59; + optional double system_up_time_awake_seconds = 60; + optional double app_up_time_including_sleep_seconds = 61; + optional double computer_time_since_last_sleep_start_seconds = 62; + optional double computer_time_since_last_sleep_end_seconds = 63; + optional bool mac_system_user_session_is_currently_active = 64; + optional double mac_system_time_since_last_user_session_deactivation_seconds = 65; + optional double mac_system_time_since_last_user_session_reactivation_seconds = 66; + optional bool application_is_currently_active = 67; + optional bool application_window_is_currently_visible = 68; + optional bool mac_application_window_is_currently_minimized = 69; + optional bool application_window_is_currently_fullscreen = 70; + optional bool mac_application_is_currently_hidden = 71; + optional bool application_user_is_currently_logged_in = 72; + optional double application_time_since_last_user_log_in = 73; + optional double application_time_since_last_user_log_out = 74; + optional bool application_is_playing_now = 75; + optional string application_currently_playing_type = 76; + optional string application_currently_playing_uri = 77; + optional string application_currently_playing_ad_id = 78; +} diff --git a/protocol/proto/Download.proto b/protocol/proto/Download.proto index 417236bd..0b3faee9 100644 --- a/protocol/proto/Download.proto +++ b/protocol/proto/Download.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -50,4 +50,6 @@ message Download { optional int64 reqs_from_cdn = 41; optional int64 error_from_cdn = 42; optional string file_origin = 43; + optional string initial_disk_state = 44; + optional bool locked = 45; } diff --git a/protocol/proto/EventSenderStats2NonAuth.proto b/protocol/proto/EventSenderStats2NonAuth.proto new file mode 100644 index 00000000..e55eaa66 --- /dev/null +++ b/protocol/proto/EventSenderStats2NonAuth.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message EventSenderStats2NonAuth { + repeated bytes sequence_ids = 1; + repeated string event_names = 2; + repeated int32 loss_stats_num_entries_per_sequence_id = 3; + repeated int32 loss_stats_event_name_index = 4; + repeated int64 loss_stats_storage_sizes = 5; + repeated int64 loss_stats_sequence_number_mins = 6; + repeated int64 loss_stats_sequence_number_nexts = 7; + repeated int32 ratelimiter_stats_event_name_index = 8; + repeated int64 ratelimiter_stats_drop_count = 9; + repeated int32 drop_list_num_entries_per_sequence_id = 10; + repeated int32 drop_list_event_name_index = 11; + repeated int64 drop_list_counts_total = 12; + repeated int64 drop_list_counts_unreported = 13; +} diff --git a/protocol/proto/HeadFileDownload.proto b/protocol/proto/HeadFileDownload.proto index acfa87fa..b0d72794 100644 --- a/protocol/proto/HeadFileDownload.proto +++ b/protocol/proto/HeadFileDownload.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -23,4 +23,5 @@ message HeadFileDownload { optional int64 bytes_from_cache = 14; optional string socket_reuse = 15; optional string request_type = 16; + optional string initial_disk_state = 17; } diff --git a/protocol/proto/LegacyEndSong.proto b/protocol/proto/LegacyEndSong.proto new file mode 100644 index 00000000..9366f18d --- /dev/null +++ b/protocol/proto/LegacyEndSong.proto @@ -0,0 +1,62 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message LegacyEndSong { + optional int64 sequence_number = 1; + optional string sequence_id = 2; + optional bytes playback_id = 3; + optional bytes parent_playback_id = 4; + optional string source_start = 5; + optional string reason_start = 6; + optional string source_end = 7; + optional string reason_end = 8; + optional int64 bytes_played = 9; + optional int64 bytes_in_song = 10; + optional int64 ms_played = 11; + optional int64 ms_nominal_played = 12; + optional int64 ms_total_est = 13; + optional int64 ms_rcv_latency = 14; + optional int64 ms_overlapping = 15; + optional int64 n_seekback = 16; + optional int64 ms_seekback = 17; + optional int64 n_seekfwd = 18; + optional int64 ms_seekfwd = 19; + optional int64 ms_latency = 20; + optional int64 ui_latency = 21; + optional string player_id = 22; + optional int64 ms_key_latency = 23; + optional bool offline_key = 24; + optional bool cached_key = 25; + optional int64 n_stutter = 26; + optional int64 p_lowbuffer = 27; + optional bool shuffle = 28; + optional int64 max_continous = 29; + optional int64 union_played = 30; + optional int64 artificial_delay = 31; + optional int64 bitrate = 32; + optional string play_context = 33; + optional string audiocodec = 34; + optional string play_track = 35; + optional string display_track = 36; + optional bool offline = 37; + optional int64 offline_timestamp = 38; + optional bool incognito_mode = 39; + optional string provider = 40; + optional string referer = 41; + optional string referrer_version = 42; + optional string referrer_vendor = 43; + optional string transition = 44; + optional string streaming_rule = 45; + optional string gaia_dev_id = 46; + optional string accepted_tc = 47; + optional string promotion_type = 48; + optional string page_instance_id = 49; + optional string interaction_id = 50; + optional string parent_play_track = 51; + optional int64 core_version = 52; +} diff --git a/protocol/proto/LocalFilesError.proto b/protocol/proto/LocalFilesError.proto index 49347341..f49d805f 100644 --- a/protocol/proto/LocalFilesError.proto +++ b/protocol/proto/LocalFilesError.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -9,4 +9,5 @@ option optimize_for = CODE_SIZE; message LocalFilesError { optional int64 error_code = 1; optional string context = 2; + optional string info = 3; } diff --git a/protocol/proto/LocalFilesImport.proto b/protocol/proto/LocalFilesImport.proto index 4deff70f..4674e721 100644 --- a/protocol/proto/LocalFilesImport.proto +++ b/protocol/proto/LocalFilesImport.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -12,4 +12,5 @@ message LocalFilesImport { optional int64 failed_tracks = 3; optional int64 matched_tracks = 4; optional string source = 5; + optional int64 invalid_tracks = 6; } diff --git a/protocol/proto/MercuryCacheReport.proto b/protocol/proto/MercuryCacheReport.proto deleted file mode 100644 index 4c9e494f..00000000 --- a/protocol/proto/MercuryCacheReport.proto +++ /dev/null @@ -1,20 +0,0 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) - -syntax = "proto2"; - -package spotify.event_sender.proto; - -option optimize_for = CODE_SIZE; - -message MercuryCacheReport { - optional int64 mercury_cache_version = 1; - optional int64 num_items = 2; - optional int64 num_locked_items = 3; - optional int64 num_expired_items = 4; - optional int64 num_lock_ids = 5; - optional int64 num_expired_lock_ids = 6; - optional int64 max_size = 7; - optional int64 total_size = 8; - optional int64 used_size = 9; - optional int64 free_size = 10; -} diff --git a/protocol/proto/ModuleDebug.proto b/protocol/proto/ModuleDebug.proto deleted file mode 100644 index 87691cd4..00000000 --- a/protocol/proto/ModuleDebug.proto +++ /dev/null @@ -1,11 +0,0 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) - -syntax = "proto2"; - -package spotify.event_sender.proto; - -option optimize_for = CODE_SIZE; - -message ModuleDebug { - optional string blob = 1; -} diff --git a/protocol/proto/OfflineUserPwdLoginNonAuth.proto b/protocol/proto/OfflineUserPwdLoginNonAuth.proto deleted file mode 100644 index 2932bd56..00000000 --- a/protocol/proto/OfflineUserPwdLoginNonAuth.proto +++ /dev/null @@ -1,11 +0,0 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) - -syntax = "proto2"; - -package spotify.event_sender.proto; - -option optimize_for = CODE_SIZE; - -message OfflineUserPwdLoginNonAuth { - optional string connection_type = 1; -} diff --git a/protocol/proto/RawCoreStream.proto b/protocol/proto/RawCoreStream.proto new file mode 100644 index 00000000..848b945b --- /dev/null +++ b/protocol/proto/RawCoreStream.proto @@ -0,0 +1,52 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message RawCoreStream { + optional bytes playback_id = 1; + optional bytes parent_playback_id = 2; + optional string video_session_id = 3; + optional bytes media_id = 4; + optional string media_type = 5; + optional string feature_identifier = 6; + optional string feature_version = 7; + optional string view_uri = 8; + optional string source_start = 9; + optional string reason_start = 10; + optional string source_end = 11; + optional string reason_end = 12; + optional int64 playback_start_time = 13; + optional int32 ms_played = 14; + optional int32 ms_played_nominal = 15; + optional int32 ms_played_overlapping = 16; + optional int32 ms_played_video = 17; + optional int32 ms_played_background = 18; + optional int32 ms_played_fullscreen = 19; + optional bool live = 20; + optional bool shuffle = 21; + optional string audio_format = 22; + optional string play_context = 23; + optional string content_uri = 24; + optional string displayed_content_uri = 25; + optional bool content_is_downloaded = 26; + optional bool incognito_mode = 27; + optional string provider = 28; + optional string referrer = 29; + optional string referrer_version = 30; + optional string referrer_vendor = 31; + optional string streaming_rule = 32; + optional string connect_controller_device_id = 33; + optional string page_instance_id = 34; + optional string interaction_id = 35; + optional string parent_content_uri = 36; + optional int64 core_version = 37; + optional string core_bundle = 38; + optional bool is_assumed_premium = 39; + optional int32 ms_played_external = 40; + optional string local_content_uri = 41; + optional bool client_offline_at_stream_start = 42; +} diff --git a/protocol/proto/anchor_extended_metadata.proto b/protocol/proto/anchor_extended_metadata.proto deleted file mode 100644 index 24d715a3..00000000 --- a/protocol/proto/anchor_extended_metadata.proto +++ /dev/null @@ -1,14 +0,0 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - -syntax = "proto3"; - -package spotify.anchor.extension; - -option objc_class_prefix = "SPT"; -option java_multiple_files = true; -option java_outer_classname = "AnchorExtensionProviderProto"; -option java_package = "com.spotify.anchorextensionprovider.proto"; - -message PodcastCounter { - uint32 counter = 1; -} diff --git a/protocol/proto/apiv1.proto b/protocol/proto/apiv1.proto index deffc3d6..2d8b9c28 100644 --- a/protocol/proto/apiv1.proto +++ b/protocol/proto/apiv1.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// No longer present in Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -39,11 +39,6 @@ message RemoveDeviceRequest { bool is_force_remove = 2; } -message RemoveDeviceResponse { - bool pending = 1; - Device device = 2; -} - message OfflineEnableDeviceResponse { Restrictions restrictions = 1; } diff --git a/protocol/proto/app_state.proto b/protocol/proto/app_state.proto new file mode 100644 index 00000000..fb4b07a4 --- /dev/null +++ b/protocol/proto/app_state.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.offline.proto; + +option optimize_for = CODE_SIZE; + +message AppStateRequest { + AppState state = 1; +} + +enum AppState { + UNKNOWN = 0; + BACKGROUND = 1; + FOREGROUND = 2; +} diff --git a/protocol/proto/autodownload_backend_service.proto b/protocol/proto/autodownload_backend_service.proto new file mode 100644 index 00000000..fa088feb --- /dev/null +++ b/protocol/proto/autodownload_backend_service.proto @@ -0,0 +1,53 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.autodownloadservice.v1.proto; + +import "google/protobuf/timestamp.proto"; + +message Identifiers { + string device_id = 1; + string cache_id = 2; +} + +message Settings { + oneof episode_download { + bool most_recent_no_limit = 1; + int32 most_recent_count = 2; + } +} + +message SetSettingsRequest { + Identifiers identifiers = 1; + Settings settings = 2; + google.protobuf.Timestamp client_timestamp = 3; +} + +message GetSettingsRequest { + Identifiers identifiers = 1; +} + +message GetSettingsResponse { + Settings settings = 1; +} + +message ShowRequest { + Identifiers identifiers = 1; + string show_uri = 2; + google.protobuf.Timestamp client_timestamp = 3; +} + +message ReplaceIdentifiersRequest { + Identifiers old_identifiers = 1; + Identifiers new_identifiers = 2; +} + +message PendingItem { + google.protobuf.Timestamp client_timestamp = 1; + + oneof pending { + bool is_removed = 2; + Settings settings = 3; + } +} diff --git a/protocol/proto/autodownload_config_common.proto b/protocol/proto/autodownload_config_common.proto new file mode 100644 index 00000000..9d923f04 --- /dev/null +++ b/protocol/proto/autodownload_config_common.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.autodownload_esperanto.proto; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.autodownload.esperanto.proto"; + +message AutoDownloadGlobalConfig { + uint32 number_of_episodes = 1; +} + +message AutoDownloadShowConfig { + string uri = 1; + bool active = 2; +} diff --git a/protocol/proto/autodownload_config_get_request.proto b/protocol/proto/autodownload_config_get_request.proto new file mode 100644 index 00000000..be4681bb --- /dev/null +++ b/protocol/proto/autodownload_config_get_request.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.autodownload_esperanto.proto; + +import "autodownload_config_common.proto"; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.autodownload.esperanto.proto"; + +message AutoDownloadGetRequest { + repeated string uri = 1; +} + +message AutoDownloadGetResponse { + AutoDownloadGlobalConfig global = 1; + repeated AutoDownloadShowConfig show = 2; + string error = 99; +} diff --git a/protocol/proto/autodownload_config_set_request.proto b/protocol/proto/autodownload_config_set_request.proto new file mode 100644 index 00000000..2adcbeab --- /dev/null +++ b/protocol/proto/autodownload_config_set_request.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.autodownload_esperanto.proto; + +import "autodownload_config_common.proto"; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.autodownload.esperanto.proto"; + +message AutoDownloadSetRequest { + oneof config { + AutoDownloadGlobalConfig global = 1; + AutoDownloadShowConfig show = 2; + } +} + +message AutoDownloadSetResponse { + string error = 99; +} diff --git a/protocol/proto/automix_mode.proto b/protocol/proto/automix_mode.proto index a4d7d66f..d0d7f938 100644 --- a/protocol/proto/automix_mode.proto +++ b/protocol/proto/automix_mode.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -6,8 +6,21 @@ package spotify.automix.proto; option optimize_for = CODE_SIZE; +message AutomixConfig { + TransitionType transition_type = 1; + string fade_out_curves = 2; + string fade_in_curves = 3; + int32 beats_min = 4; + int32 beats_max = 5; + int32 fade_duration_max_ms = 6; +} + message AutomixMode { AutomixStyle style = 1; + AutomixConfig config = 2; + AutomixConfig ml_config = 3; + AutomixConfig shuffle_config = 4; + AutomixConfig shuffle_ml_config = 5; } enum AutomixStyle { @@ -18,4 +31,11 @@ enum AutomixStyle { RADIO_AIRBAG = 4; SLEEP = 5; MIXED = 6; + CUSTOM = 7; +} + +enum TransitionType { + CUEPOINTS = 0; + CROSSFADE = 1; + GAPLESS = 2; } diff --git a/protocol/proto/canvas_storage.proto b/protocol/proto/canvas_storage.proto new file mode 100644 index 00000000..e2f652c2 --- /dev/null +++ b/protocol/proto/canvas_storage.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.canvas.proto.storage; + +import "canvaz.proto"; + +option optimize_for = CODE_SIZE; + +message CanvasCacheEntry { + string entity_uri = 1; + uint64 expires_on_seconds = 2; + canvaz.cache.EntityCanvazResponse.Canvaz canvas = 3; +} + +message CanvasCacheFile { + repeated CanvasCacheEntry entries = 1; +} diff --git a/protocol/proto/canvaz-meta.proto b/protocol/proto/canvaz-meta.proto index 540daeb6..b3b55531 100644 --- a/protocol/proto/canvaz-meta.proto +++ b/protocol/proto/canvaz-meta.proto @@ -1,9 +1,12 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + syntax = "proto3"; -package com.spotify.canvaz; +package spotify.canvaz; +option java_multiple_files = true; option optimize_for = CODE_SIZE; -option java_package = "com.spotify.canvaz"; +option java_package = "com.spotify.canvaz.proto"; enum Type { IMAGE = 0; @@ -11,4 +14,4 @@ enum Type { VIDEO_LOOPING = 2; VIDEO_LOOPING_RANDOM = 3; GIF = 4; -} \ No newline at end of file +} diff --git a/protocol/proto/canvaz.proto b/protocol/proto/canvaz.proto index ca283ab5..2493da95 100644 --- a/protocol/proto/canvaz.proto +++ b/protocol/proto/canvaz.proto @@ -1,11 +1,14 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + syntax = "proto3"; -package com.spotify.canvazcache; +package spotify.canvaz.cache; import "canvaz-meta.proto"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; -option java_package = "com.spotify.canvaz"; +option java_package = "com.spotify.canvazcache.proto"; message Artist { string uri = 1; @@ -19,15 +22,16 @@ message EntityCanvazResponse { string id = 1; string url = 2; string file_id = 3; - com.spotify.canvaz.Type type = 4; + spotify.canvaz.Type type = 4; string entity_uri = 5; Artist artist = 6; bool explicit = 7; string uploaded_by = 8; string etag = 9; string canvas_uri = 11; + string storylines_id = 12; } - + int64 ttl_in_seconds = 2; } @@ -37,4 +41,4 @@ message EntityCanvazRequest { string entity_uri = 1; string etag = 2; } -} \ No newline at end of file +} diff --git a/protocol/proto/client-tts.proto b/protocol/proto/client-tts.proto new file mode 100644 index 00000000..0968f515 --- /dev/null +++ b/protocol/proto/client-tts.proto @@ -0,0 +1,30 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.narration_injection.proto; + +import "tts-resolve.proto"; + +option optimize_for = CODE_SIZE; + +service ClientTtsService { + rpc GetTtsUrl(TtsRequest) returns (TtsResponse); +} + +message TtsRequest { + ResolveRequest.AudioFormat audio_format = 3; + string language = 4; + ResolveRequest.TtsVoice tts_voice = 5; + ResolveRequest.TtsProvider tts_provider = 6; + int32 sample_rate_hz = 7; + + oneof prompt { + string text = 1; + string ssml = 2; + } +} + +message TtsResponse { + string url = 1; +} diff --git a/protocol/proto/client_config.proto b/protocol/proto/client_config.proto new file mode 100644 index 00000000..b838873e --- /dev/null +++ b/protocol/proto/client_config.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.extendedmetadata.config.v1; + +option optimize_for = CODE_SIZE; + +message ClientConfig { + uint32 log_sampling_rate = 1; + uint32 avg_log_messages_per_minute = 2; + uint32 log_messages_burst_size = 3; +} diff --git a/protocol/proto/cloud_host_messages.proto b/protocol/proto/cloud_host_messages.proto deleted file mode 100644 index 49949188..00000000 --- a/protocol/proto/cloud_host_messages.proto +++ /dev/null @@ -1,152 +0,0 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) - -syntax = "proto3"; - -package spotify.social_listening.cloud_host; - -option objc_class_prefix = "CloudHost"; -option optimize_for = CODE_SIZE; -option java_package = "com.spotify.social_listening.cloud_host"; - -message LookupSessionRequest { - string token = 1; - JoinType join_type = 2; -} - -message LookupSessionResponse { - oneof response { - Session session = 1; - ErrorCode error = 2; - } -} - -message CreateSessionRequest { - -} - -message CreateSessionResponse { - oneof response { - Session session = 1; - ErrorCode error = 2; - } -} - -message DeleteSessionRequest { - string session_id = 1; -} - -message DeleteSessionResponse { - oneof response { - Session session = 1; - ErrorCode error = 2; - } -} - -message JoinSessionRequest { - string join_token = 1; - Experience experience = 3; -} - -message JoinSessionResponse { - oneof response { - Session session = 1; - ErrorCode error = 2; - } -} - -message LeaveSessionRequest { - string session_id = 1; -} - -message LeaveSessionResponse { - oneof response { - Session session = 1; - ErrorCode error = 2; - } -} - -message GetCurrentSessionRequest { - -} - -message GetCurrentSessionResponse { - oneof response { - Session session = 1; - ErrorCode error = 2; - } -} - -message SessionUpdateRequest { - -} - -message SessionUpdate { - Session session = 1; - SessionUpdateReason reason = 3; - repeated SessionMember updated_session_members = 4; -} - -message SessionUpdateResponse { - oneof response { - SessionUpdate session_update = 1; - ErrorCode error = 2; - } -} - -message Session { - int64 timestamp = 1; - string session_id = 2; - string join_session_token = 3; - string join_session_url = 4; - string session_owner_id = 5; - repeated SessionMember session_members = 6; - string join_session_uri = 7; - bool is_session_owner = 8; -} - -message SessionMember { - int64 timestamp = 1; - string member_id = 2; - string username = 3; - string display_name = 4; - string image_url = 5; - string large_image_url = 6; - bool current_user = 7; -} - -enum JoinType { - NotSpecified = 0; - Scanning = 1; - DeepLinking = 2; - DiscoveredDevice = 3; - Frictionless = 4; - NearbyWifi = 5; -} - -enum ErrorCode { - Unknown = 0; - ParseError = 1; - JoinFailed = 1000; - SessionFull = 1001; - FreeUser = 1002; - ScannableError = 1003; - JoinExpiredSession = 1004; - NoExistingSession = 1005; -} - -enum Experience { - UNKNOWN = 0; - BEETHOVEN = 1; - BACH = 2; -} - -enum SessionUpdateReason { - UNKNOWN_UPDATE_REASON = 0; - NEW_SESSION = 1; - USER_JOINED = 2; - USER_LEFT = 3; - SESSION_DELETED = 4; - YOU_LEFT = 5; - YOU_WERE_KICKED = 6; - YOU_JOINED = 7; -} diff --git a/protocol/proto/collection/episode_collection_state.proto b/protocol/proto/collection/episode_collection_state.proto index 403bfbb4..56fcc533 100644 --- a/protocol/proto/collection/episode_collection_state.proto +++ b/protocol/proto/collection/episode_collection_state.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.cosmos_util.proto; +option objc_class_prefix = "SPTCosmosUtil"; option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "com.spotify.cosmos.util.proto"; diff --git a/protocol/proto/collection_add_remove_items_request.proto b/protocol/proto/collection_add_remove_items_request.proto new file mode 100644 index 00000000..4dac680e --- /dev/null +++ b/protocol/proto/collection_add_remove_items_request.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.collection_cosmos.proto; + +import "status.proto"; + +option optimize_for = CODE_SIZE; + +message CollectionAddRemoveItemsRequest { + repeated string item = 1; +} + +message CollectionAddRemoveItemsResponse { + Status status = 1; +} diff --git a/protocol/proto/collection_ban_request.proto b/protocol/proto/collection_ban_request.proto new file mode 100644 index 00000000..e64220df --- /dev/null +++ b/protocol/proto/collection_ban_request.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.collection_cosmos.proto; + +import "status.proto"; + +option optimize_for = CODE_SIZE; + +message CollectionBanRequest { + string context_source = 1; + repeated string uri = 2; +} + +message CollectionBanResponse { + Status status = 1; + repeated bool success = 2; +} diff --git a/protocol/proto/collection_decoration_policy.proto b/protocol/proto/collection_decoration_policy.proto new file mode 100644 index 00000000..79b4b8cf --- /dev/null +++ b/protocol/proto/collection_decoration_policy.proto @@ -0,0 +1,38 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.collection_cosmos.proto; + +import "policy/artist_decoration_policy.proto"; +import "policy/album_decoration_policy.proto"; +import "policy/track_decoration_policy.proto"; + +option optimize_for = CODE_SIZE; + +message CollectionArtistDecorationPolicy { + cosmos_util.proto.ArtistCollectionDecorationPolicy collection_policy = 1; + cosmos_util.proto.ArtistSyncDecorationPolicy sync_policy = 2; + cosmos_util.proto.ArtistDecorationPolicy artist_policy = 3; + bool decorated = 4; +} + +message CollectionAlbumDecorationPolicy { + bool decorated = 1; + bool album_type = 2; + CollectionArtistDecorationPolicy artist_policy = 3; + CollectionArtistDecorationPolicy artists_policy = 4; + cosmos_util.proto.AlbumCollectionDecorationPolicy collection_policy = 5; + cosmos_util.proto.AlbumSyncDecorationPolicy sync_policy = 6; + cosmos_util.proto.AlbumDecorationPolicy album_policy = 7; +} + +message CollectionTrackDecorationPolicy { + cosmos_util.proto.TrackCollectionDecorationPolicy collection_policy = 1; + cosmos_util.proto.TrackSyncDecorationPolicy sync_policy = 2; + cosmos_util.proto.TrackDecorationPolicy track_policy = 3; + cosmos_util.proto.TrackPlayedStateDecorationPolicy played_state_policy = 4; + CollectionAlbumDecorationPolicy album_policy = 5; + cosmos_util.proto.ArtistDecorationPolicy artist_policy = 6; + bool decorated = 7; +} diff --git a/protocol/proto/collection_get_bans_request.proto b/protocol/proto/collection_get_bans_request.proto new file mode 100644 index 00000000..a67574ae --- /dev/null +++ b/protocol/proto/collection_get_bans_request.proto @@ -0,0 +1,33 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.collection_cosmos.proto; + +import "policy/track_decoration_policy.proto"; +import "policy/artist_decoration_policy.proto"; +import "metadata/track_metadata.proto"; +import "metadata/artist_metadata.proto"; +import "status.proto"; + +option objc_class_prefix = "SPTCollectionCosmos"; +option optimize_for = CODE_SIZE; + +message CollectionGetBansRequest { + cosmos_util.proto.TrackDecorationPolicy track_policy = 1; + cosmos_util.proto.ArtistDecorationPolicy artist_policy = 2; + string sort = 3; + bool timestamp = 4; + uint32 update_throttling = 5; +} + +message Item { + uint32 add_time = 1; + cosmos_util.proto.TrackMetadata track_metadata = 2; + cosmos_util.proto.ArtistMetadata artist_metadata = 3; +} + +message CollectionGetBansResponse { + Status status = 1; + repeated Item item = 2; +} diff --git a/protocol/proto/collection_index.proto b/protocol/proto/collection_index.proto index 5af95a35..ee6b3efc 100644 --- a/protocol/proto/collection_index.proto +++ b/protocol/proto/collection_index.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -12,7 +12,7 @@ message IndexRepairerState { } message CollectionTrackEntry { - string track_uri = 1; + string uri = 1; string track_name = 2; string album_uri = 3; string album_name = 4; @@ -23,18 +23,16 @@ message CollectionTrackEntry { int64 add_time = 9; } -message CollectionAlbumEntry { - string album_uri = 1; +message CollectionAlbumLikeEntry { + string uri = 1; string album_name = 2; - string album_image_uri = 3; - string artist_uri = 4; - string artist_name = 5; + string creator_uri = 4; + string creator_name = 5; int64 add_time = 6; } -message CollectionMetadataMigratorState { - bytes last_checked_key = 1; - bool migrated_tracks = 2; - bool migrated_albums = 3; - bool migrated_album_tracks = 4; +message CollectionArtistEntry { + string uri = 1; + string artist_name = 2; + int64 add_time = 4; } diff --git a/protocol/proto/collection_item.proto b/protocol/proto/collection_item.proto new file mode 100644 index 00000000..4a98e9d0 --- /dev/null +++ b/protocol/proto/collection_item.proto @@ -0,0 +1,48 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.collection_cosmos.proto; + +import "metadata/album_metadata.proto"; +import "metadata/artist_metadata.proto"; +import "metadata/track_metadata.proto"; +import "collection/artist_collection_state.proto"; +import "collection/album_collection_state.proto"; +import "collection/track_collection_state.proto"; +import "sync/artist_sync_state.proto"; +import "sync/album_sync_state.proto"; +import "sync/track_sync_state.proto"; +import "played_state/track_played_state.proto"; + +option optimize_for = CODE_SIZE; + +message CollectionTrack { + uint32 index = 1; + uint32 add_time = 2; + cosmos_util.proto.TrackMetadata track_metadata = 3; + cosmos_util.proto.TrackCollectionState track_collection_state = 4; + cosmos_util.proto.TrackPlayState track_play_state = 5; + cosmos_util.proto.TrackSyncState track_sync_state = 6; + bool decorated = 7; + CollectionAlbum album = 8; + string cover = 9; +} + +message CollectionAlbum { + uint32 add_time = 1; + cosmos_util.proto.AlbumMetadata album_metadata = 2; + cosmos_util.proto.AlbumCollectionState album_collection_state = 3; + cosmos_util.proto.AlbumSyncState album_sync_state = 4; + bool decorated = 5; + string album_type = 6; + repeated CollectionTrack track = 7; +} + +message CollectionArtist { + cosmos_util.proto.ArtistMetadata artist_metadata = 1; + cosmos_util.proto.ArtistCollectionState artist_collection_state = 2; + cosmos_util.proto.ArtistSyncState artist_sync_state = 3; + bool decorated = 4; + repeated CollectionAlbum album = 5; +} diff --git a/protocol/proto/collection_platform_requests.proto b/protocol/proto/collection_platform_requests.proto index efe9a847..a855c217 100644 --- a/protocol/proto/collection_platform_requests.proto +++ b/protocol/proto/collection_platform_requests.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -6,10 +6,6 @@ package spotify.collection_platform.proto; option optimize_for = CODE_SIZE; -message CollectionPlatformSimpleRequest { - CollectionSet set = 1; -} - message CollectionPlatformItemsRequest { CollectionSet set = 1; repeated string items = 2; @@ -21,4 +17,5 @@ enum CollectionSet { BAN = 2; LISTENLATER = 3; IGNOREINRECS = 4; + ENHANCED = 5; } diff --git a/protocol/proto/collection_platform_responses.proto b/protocol/proto/collection_platform_responses.proto index fd236c12..6b7716d8 100644 --- a/protocol/proto/collection_platform_responses.proto +++ b/protocol/proto/collection_platform_responses.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -10,8 +10,13 @@ message CollectionPlatformSimpleResponse { string error_msg = 1; } +message CollectionPlatformItem { + string uri = 1; + int64 add_time = 2; +} + message CollectionPlatformItemsResponse { - repeated string items = 1; + repeated CollectionPlatformItem items = 1; } message CollectionPlatformContainsResponse { diff --git a/protocol/proto/collection_storage.proto b/protocol/proto/collection_storage.proto deleted file mode 100644 index 1dd4f034..00000000 --- a/protocol/proto/collection_storage.proto +++ /dev/null @@ -1,20 +0,0 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - -syntax = "proto2"; - -package spotify.collection.proto.storage; - -import "collection2.proto"; - -option optimize_for = CODE_SIZE; - -message CollectionHeader { - optional bytes etag = 1; -} - -message CollectionCache { - optional CollectionHeader header = 1; - optional CollectionItems collection = 2; - optional CollectionItems pending = 3; - optional uint32 collection_item_limit = 4; -} diff --git a/protocol/proto/composite_formats_node.proto b/protocol/proto/composite_formats_node.proto deleted file mode 100644 index 75717c98..00000000 --- a/protocol/proto/composite_formats_node.proto +++ /dev/null @@ -1,31 +0,0 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) - -syntax = "proto2"; - -package spotify.player.proto; - -import "track_instance.proto"; -import "track_instantiator.proto"; - -option optimize_for = CODE_SIZE; - -message InjectionSegment { - required string track_uri = 1; - optional int64 start = 2; - optional int64 stop = 3; - required int64 duration = 4; -} - -message InjectionModel { - required string episode_uri = 1; - required int64 total_duration = 2; - repeated InjectionSegment segments = 3; -} - -message CompositeFormatsPrototypeNode { - required string mode = 1; - optional InjectionModel injection_model = 2; - required uint32 index = 3; - required TrackInstantiator instantiator = 4; - optional TrackInstance track = 5; -} diff --git a/protocol/proto/connect.proto b/protocol/proto/connect.proto index dae2561a..d6485252 100644 --- a/protocol/proto/connect.proto +++ b/protocol/proto/connect.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -87,6 +87,9 @@ message DeviceInfo { string public_ip = 22; string license = 23; bool is_group = 25; + bool is_dynamic_device = 26; + repeated string disallow_playback_reasons = 27; + repeated string disallow_transfer_reasons = 28; oneof _audio_output_device_info { AudioOutputDeviceInfo audio_output_device_info = 24; @@ -133,8 +136,9 @@ message Capabilities { bool supports_gzip_pushes = 23; bool supports_set_options_command = 25; CapabilitySupportDetails supports_hifi = 26; + string connect_capabilities = 27; - // reserved 1, 4, 24, "supported_contexts", "supports_lossless_audio"; + //reserved 1, 4, 24, "supported_contexts", "supports_lossless_audio"; } message CapabilitySupportDetails { diff --git a/protocol/proto/context_application_desktop.proto b/protocol/proto/context_application_desktop.proto new file mode 100644 index 00000000..04f443b2 --- /dev/null +++ b/protocol/proto/context_application_desktop.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ApplicationDesktop { + string version_string = 1; + int64 version_code = 2; +} diff --git a/protocol/proto/context_core.proto b/protocol/proto/context_core.proto deleted file mode 100644 index 1e49afaf..00000000 --- a/protocol/proto/context_core.proto +++ /dev/null @@ -1,14 +0,0 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) - -syntax = "proto3"; - -package spotify.event_sender.proto; - -option optimize_for = CODE_SIZE; - -message Core { - string os_name = 1; - string os_version = 2; - string device_id = 3; - string client_version = 4; -} diff --git a/protocol/proto/context_device_desktop.proto b/protocol/proto/context_device_desktop.proto new file mode 100644 index 00000000..a6b38372 --- /dev/null +++ b/protocol/proto/context_device_desktop.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DeviceDesktop { + string platform_type = 1; + string device_manufacturer = 2; + string device_model = 3; + string device_id = 4; + string os_version = 5; +} diff --git a/protocol/proto/context_node.proto b/protocol/proto/context_node.proto index 8ff3cb28..82dd9d62 100644 --- a/protocol/proto/context_node.proto +++ b/protocol/proto/context_node.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -20,4 +20,5 @@ message ContextNode { optional ContextProcessor context_processor = 6; optional string session_id = 7; optional sint32 iteration = 8; + optional bool pending_pause = 9; } diff --git a/protocol/proto/context_player_ng.proto b/protocol/proto/context_player_ng.proto deleted file mode 100644 index e61f011e..00000000 --- a/protocol/proto/context_player_ng.proto +++ /dev/null @@ -1,12 +0,0 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) - -syntax = "proto2"; - -package spotify.player.proto; - -option optimize_for = CODE_SIZE; - -message ContextPlayerNg { - map player_model = 1; - optional uint64 playback_position = 2; -} diff --git a/protocol/proto/context_sdk.proto b/protocol/proto/context_sdk.proto index dc5d3236..419f7aa5 100644 --- a/protocol/proto/context_sdk.proto +++ b/protocol/proto/context_sdk.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -8,4 +8,5 @@ option optimize_for = CODE_SIZE; message Sdk { string version_name = 1; + string type = 2; } diff --git a/protocol/proto/core_configuration_applied_non_auth.proto b/protocol/proto/core_configuration_applied_non_auth.proto deleted file mode 100644 index d7c132dc..00000000 --- a/protocol/proto/core_configuration_applied_non_auth.proto +++ /dev/null @@ -1,11 +0,0 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - -syntax = "proto3"; - -package spotify.remote_config.proto; - -option optimize_for = CODE_SIZE; - -message CoreConfigurationAppliedNonAuth { - string configuration_assignment_id = 1; -} diff --git a/protocol/proto/cosmos_changes_request.proto b/protocol/proto/cosmos_changes_request.proto index 47cd584f..2e4b7040 100644 --- a/protocol/proto/cosmos_changes_request.proto +++ b/protocol/proto/cosmos_changes_request.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.collection_cosmos.changes_request.proto; +option objc_class_prefix = "SPTCollectionCosmosChanges"; option optimize_for = CODE_SIZE; message Response { diff --git a/protocol/proto/cosmos_decorate_request.proto b/protocol/proto/cosmos_decorate_request.proto index 2709b30a..9e586021 100644 --- a/protocol/proto/cosmos_decorate_request.proto +++ b/protocol/proto/cosmos_decorate_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -22,6 +22,7 @@ import "metadata/episode_metadata.proto"; import "metadata/show_metadata.proto"; import "metadata/track_metadata.proto"; +option objc_class_prefix = "SPTCollectionCosmosDecorate"; option optimize_for = CODE_SIZE; message Album { diff --git a/protocol/proto/cosmos_get_album_list_request.proto b/protocol/proto/cosmos_get_album_list_request.proto index 741e9f49..448dcd46 100644 --- a/protocol/proto/cosmos_get_album_list_request.proto +++ b/protocol/proto/cosmos_get_album_list_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -8,6 +8,7 @@ import "collection/album_collection_state.proto"; import "sync/album_sync_state.proto"; import "metadata/album_metadata.proto"; +option objc_class_prefix = "SPTCollectionCosmosAlbumList"; option optimize_for = CODE_SIZE; message Item { diff --git a/protocol/proto/cosmos_get_artist_list_request.proto b/protocol/proto/cosmos_get_artist_list_request.proto index b8ccb662..1dfeedba 100644 --- a/protocol/proto/cosmos_get_artist_list_request.proto +++ b/protocol/proto/cosmos_get_artist_list_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -8,6 +8,7 @@ import "collection/artist_collection_state.proto"; import "sync/artist_sync_state.proto"; import "metadata/artist_metadata.proto"; +option objc_class_prefix = "SPTCollectionCosmosArtistList"; option optimize_for = CODE_SIZE; message Item { diff --git a/protocol/proto/cosmos_get_episode_list_request.proto b/protocol/proto/cosmos_get_episode_list_request.proto index 8168fbfe..437a621f 100644 --- a/protocol/proto/cosmos_get_episode_list_request.proto +++ b/protocol/proto/cosmos_get_episode_list_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -9,6 +9,7 @@ import "played_state/episode_played_state.proto"; import "sync/episode_sync_state.proto"; import "metadata/episode_metadata.proto"; +option objc_class_prefix = "SPTCollectionCosmosEpisodeList"; option optimize_for = CODE_SIZE; message Item { diff --git a/protocol/proto/cosmos_get_show_list_request.proto b/protocol/proto/cosmos_get_show_list_request.proto index 880f7cea..e2b8a578 100644 --- a/protocol/proto/cosmos_get_show_list_request.proto +++ b/protocol/proto/cosmos_get_show_list_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -8,6 +8,7 @@ import "collection/show_collection_state.proto"; import "played_state/show_played_state.proto"; import "metadata/show_metadata.proto"; +option objc_class_prefix = "SPTCollectionCosmosShowList"; option optimize_for = CODE_SIZE; message Item { diff --git a/protocol/proto/cosmos_get_tags_info_request.proto b/protocol/proto/cosmos_get_tags_info_request.proto index fe666025..5480c7bc 100644 --- a/protocol/proto/cosmos_get_tags_info_request.proto +++ b/protocol/proto/cosmos_get_tags_info_request.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.collection_cosmos.tags_info_request.proto; +option objc_class_prefix = "SPTCollectionCosmosTagsInfo"; option optimize_for = CODE_SIZE; message Response { diff --git a/protocol/proto/cosmos_get_track_list_metadata_request.proto b/protocol/proto/cosmos_get_track_list_metadata_request.proto index 8a02c962..a4586249 100644 --- a/protocol/proto/cosmos_get_track_list_metadata_request.proto +++ b/protocol/proto/cosmos_get_track_list_metadata_request.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.collection_cosmos.proto; +option objc_class_prefix = "SPTCollectionCosmos"; option optimize_for = CODE_SIZE; message TrackListMetadata { diff --git a/protocol/proto/cosmos_get_track_list_request.proto b/protocol/proto/cosmos_get_track_list_request.proto index c92320f7..95c83410 100644 --- a/protocol/proto/cosmos_get_track_list_request.proto +++ b/protocol/proto/cosmos_get_track_list_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -9,6 +9,7 @@ import "played_state/track_played_state.proto"; import "sync/track_sync_state.proto"; import "metadata/track_metadata.proto"; +option objc_class_prefix = "SPTCollectionCosmosTrackList"; option optimize_for = CODE_SIZE; message Item { diff --git a/protocol/proto/cosmos_get_unplayed_episodes_request.proto b/protocol/proto/cosmos_get_unplayed_episodes_request.proto index 8957ae56..09339c78 100644 --- a/protocol/proto/cosmos_get_unplayed_episodes_request.proto +++ b/protocol/proto/cosmos_get_unplayed_episodes_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -9,6 +9,7 @@ import "played_state/episode_played_state.proto"; import "sync/episode_sync_state.proto"; import "metadata/episode_metadata.proto"; +option objc_class_prefix = "SPTCollectionCosmosUnplayedEpisodes"; option optimize_for = CODE_SIZE; message Item { diff --git a/protocol/proto/decorate_request.proto b/protocol/proto/decorate_request.proto index cad3f526..ff1fa0ed 100644 --- a/protocol/proto/decorate_request.proto +++ b/protocol/proto/decorate_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -6,6 +6,7 @@ package spotify.show_cosmos.decorate_request.proto; import "metadata/episode_metadata.proto"; import "metadata/show_metadata.proto"; +import "played_state/episode_played_state.proto"; import "show_access.proto"; import "show_episode_state.proto"; import "show_show_state.proto"; @@ -14,8 +15,11 @@ import "podcast_virality.proto"; import "podcastextensions.proto"; import "podcast_poll.proto"; import "podcast_qna.proto"; +import "podcast_ratings.proto"; import "transcripts.proto"; +import "clips_cover.proto"; +option objc_class_prefix = "SPTShowCosmosDecorate"; option optimize_for = CODE_SIZE; message Show { @@ -24,13 +28,14 @@ message Show { optional show_cosmos.proto.ShowPlayState show_play_state = 3; optional string link = 4; optional podcast_paywalls.ShowAccess access_info = 5; + optional ratings.PodcastRating podcast_rating = 6; } message Episode { optional cosmos_util.proto.EpisodeMetadata episode_metadata = 1; optional show_cosmos.proto.EpisodeCollectionState episode_collection_state = 2; optional show_cosmos.proto.EpisodeOfflineState episode_offline_state = 3; - optional show_cosmos.proto.EpisodePlayState episode_play_state = 4; + optional cosmos_util.proto.EpisodePlayState episode_play_state = 4; optional string link = 5; optional podcast_segments.PodcastSegments segments = 6; optional podcast.extensions.PodcastHtmlDescription html_description = 7; @@ -38,6 +43,7 @@ message Episode { optional podcastvirality.v1.PodcastVirality virality = 10; optional polls.PodcastPoll podcast_poll = 11; optional qanda.PodcastQna podcast_qna = 12; + optional clips.ClipsCover clips = 13; reserved 8; } diff --git a/protocol/proto/dependencies/session_control.proto b/protocol/proto/dependencies/session_control.proto deleted file mode 100644 index f4e6d744..00000000 --- a/protocol/proto/dependencies/session_control.proto +++ /dev/null @@ -1,121 +0,0 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) - -syntax = "proto3"; - -package com.spotify.sessioncontrol.api.v1; - -option java_multiple_files = true; -option optimize_for = CODE_SIZE; -option java_package = "com.spotify.sessioncontrol.api.v1.proto"; - -service SessionControlService { - rpc GetCurrentSession(GetCurrentSessionRequest) returns (GetCurrentSessionResponse); - rpc GetCurrentSessionOrNew(GetCurrentSessionOrNewRequest) returns (GetCurrentSessionOrNewResponse); - rpc JoinSession(JoinSessionRequest) returns (JoinSessionResponse); - rpc GetSessionInfo(GetSessionInfoRequest) returns (GetSessionInfoResponse); - rpc LeaveSession(LeaveSessionRequest) returns (LeaveSessionResponse); - rpc EndSession(EndSessionRequest) returns (EndSessionResponse); - rpc VerifyCommand(VerifyCommandRequest) returns (VerifyCommandResponse); -} - -message SessionUpdate { - Session session = 1; - SessionUpdateReason reason = 2; - repeated SessionMember updated_session_members = 3; -} - -message GetCurrentSessionRequest { - -} - -message GetCurrentSessionResponse { - Session session = 1; -} - -message GetCurrentSessionOrNewRequest { - string fallback_device_id = 1; -} - -message GetCurrentSessionOrNewResponse { - Session session = 1; -} - -message JoinSessionRequest { - string join_token = 1; - string device_id = 2; - Experience experience = 3; -} - -message JoinSessionResponse { - Session session = 1; -} - -message GetSessionInfoRequest { - string join_token = 1; -} - -message GetSessionInfoResponse { - Session session = 1; -} - -message LeaveSessionRequest { - -} - -message LeaveSessionResponse { - -} - -message EndSessionRequest { - string session_id = 1; -} - -message EndSessionResponse { - -} - -message VerifyCommandRequest { - string session_id = 1; - string command = 2; -} - -message VerifyCommandResponse { - bool allowed = 1; -} - -message Session { - int64 timestamp = 1; - string session_id = 2; - string join_session_token = 3; - string join_session_url = 4; - string session_owner_id = 5; - repeated SessionMember session_members = 6; - string join_session_uri = 7; - bool is_session_owner = 8; -} - -message SessionMember { - int64 timestamp = 1; - string id = 2; - string username = 3; - string display_name = 4; - string image_url = 5; - string large_image_url = 6; -} - -enum SessionUpdateReason { - UNKNOWN_UPDATE_REASON = 0; - NEW_SESSION = 1; - USER_JOINED = 2; - USER_LEFT = 3; - SESSION_DELETED = 4; - YOU_LEFT = 5; - YOU_WERE_KICKED = 6; - YOU_JOINED = 7; -} - -enum Experience { - UNKNOWN = 0; - BEETHOVEN = 1; - BACH = 2; -} diff --git a/protocol/proto/display_segments_extension.proto b/protocol/proto/display_segments_extension.proto new file mode 100644 index 00000000..04714446 --- /dev/null +++ b/protocol/proto/display_segments_extension.proto @@ -0,0 +1,54 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.displaysegments.v1; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "DisplaySegmentsExtensionProto"; +option java_package = "com.spotify.displaysegments.v1.proto"; + +message DisplaySegmentsExtension { + string episode_uri = 1; + repeated DisplaySegment segments = 2; + int32 duration_ms = 3; + + oneof decoration { + MusicAndTalkDecoration music_and_talk_decoration = 4; + } +} + +message DisplaySegment { + string uri = 1; + SegmentType type = 2; + int32 duration_ms = 3; + int32 seek_start_ms = 4; + int32 seek_stop_ms = 5; + + oneof _title { + string title = 6; + } + + oneof _subtitle { + string subtitle = 7; + } + + oneof _image_url { + string image_url = 8; + } + + oneof _is_preview { + bool is_preview = 9; + } +} + +message MusicAndTalkDecoration { + bool can_upsell = 1; +} + +enum SegmentType { + SEGMENT_TYPE_UNSPECIFIED = 0; + SEGMENT_TYPE_TALK = 1; + SEGMENT_TYPE_MUSIC = 2; +} diff --git a/protocol/proto/es_command_options.proto b/protocol/proto/es_command_options.proto index c261ca27..0a37e801 100644 --- a/protocol/proto/es_command_options.proto +++ b/protocol/proto/es_command_options.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -12,4 +12,5 @@ message CommandOptions { bool override_restrictions = 1; bool only_for_local_device = 2; bool system_initiated = 3; + bytes only_for_playback_id = 4; } diff --git a/protocol/proto/es_ident.proto b/protocol/proto/es_ident.proto new file mode 100644 index 00000000..6c52abc2 --- /dev/null +++ b/protocol/proto/es_ident.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.connectivity.pubsub.esperanto.proto; + +option java_package = "com.spotify.connectivity.pubsub.esperanto.proto"; + +message Ident { + string Ident = 1; +} diff --git a/protocol/proto/es_ident_filter.proto b/protocol/proto/es_ident_filter.proto new file mode 100644 index 00000000..19ccee40 --- /dev/null +++ b/protocol/proto/es_ident_filter.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.connectivity.pubsub.esperanto.proto; + +option java_package = "com.spotify.connectivity.pubsub.esperanto.proto"; + +message IdentFilter { + string Prefix = 1; +} diff --git a/protocol/proto/es_prefs.proto b/protocol/proto/es_prefs.proto new file mode 100644 index 00000000..f81916ca --- /dev/null +++ b/protocol/proto/es_prefs.proto @@ -0,0 +1,53 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.prefs.esperanto.proto; + +option objc_class_prefix = "ESP"; +option java_package = "com.spotify.prefs.esperanto.proto"; + +service Prefs { + rpc Get(GetParams) returns (PrefValues); + rpc Sub(SubParams) returns (stream PrefValues); + rpc GetAll(GetAllParams) returns (PrefValues); + rpc SubAll(SubAllParams) returns (stream PrefValues); + rpc Set(SetParams) returns (PrefValues); + rpc Create(CreateParams) returns (PrefValues); +} + +message GetParams { + string key = 1; +} + +message SubParams { + string key = 1; +} + +message GetAllParams { + +} + +message SubAllParams { + +} + +message Value { + oneof value { + int64 number = 1; + bool bool = 2; + string string = 3; + } +} + +message SetParams { + map entries = 1; +} + +message CreateParams { + map entries = 1; +} + +message PrefValues { + map entries = 1; +} diff --git a/protocol/proto/es_pushed_message.proto b/protocol/proto/es_pushed_message.proto new file mode 100644 index 00000000..dd054f5f --- /dev/null +++ b/protocol/proto/es_pushed_message.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.connectivity.pubsub.esperanto.proto; + +import "es_ident.proto"; + +option java_package = "com.spotify.connectivity.pubsub.esperanto.proto"; + +message PushedMessage { + Ident Ident = 1; + repeated string Payloads = 2; + map Attributes = 3; +} diff --git a/protocol/proto/es_remote_config.proto b/protocol/proto/es_remote_config.proto new file mode 100644 index 00000000..fca7f0f9 --- /dev/null +++ b/protocol/proto/es_remote_config.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.remote_config.esperanto.proto; + +option objc_class_prefix = "ESP"; +option java_package = "com.spotify.remoteconfig.esperanto.proto"; + +service RemoteConfig { + rpc lookupBool(LookupRequest) returns (BoolResponse); +} + +message LookupRequest { + string component_id = 1; + string key = 2; +} + +message BoolResponse { + bool value = 1; +} diff --git a/protocol/proto/es_request_info.proto b/protocol/proto/es_request_info.proto new file mode 100644 index 00000000..95b5cb81 --- /dev/null +++ b/protocol/proto/es_request_info.proto @@ -0,0 +1,27 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.connectivity.netstat.esperanto.proto; + +option java_package = "com.spotify.connectivity.netstat.esperanto.proto"; + +message RepeatedRequestInfo { + repeated RequestInfo infos = 1; +} + +message RequestInfo { + string uri = 1; + string verb = 2; + string source_identifier = 3; + int32 downloaded = 4; + int32 uploaded = 5; + int32 payload_size = 6; + bool connection_reuse = 7; + int64 event_started = 8; + int64 event_connected = 9; + int64 event_request_sent = 10; + int64 event_first_byte_received = 11; + int64 event_last_byte_received = 12; + int64 event_ended = 13; +} diff --git a/protocol/proto/es_seek_to.proto b/protocol/proto/es_seek_to.proto index 0ef8aa4b..59073cf9 100644 --- a/protocol/proto/es_seek_to.proto +++ b/protocol/proto/es_seek_to.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -15,4 +15,11 @@ message SeekToRequest { CommandOptions options = 1; LoggingParams logging_params = 2; int64 position = 3; + + Relative relative = 4; + enum Relative { + BEGINNING = 0; + END = 1; + CURRENT = 2; + } } diff --git a/protocol/proto/es_storage.proto b/protocol/proto/es_storage.proto new file mode 100644 index 00000000..c20b3be7 --- /dev/null +++ b/protocol/proto/es_storage.proto @@ -0,0 +1,88 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.storage.esperanto.proto; + +import "google/protobuf/empty.proto"; + +option objc_class_prefix = "ESP"; +option java_package = "com.spotify.storage.esperanto.proto"; + +service Storage { + rpc GetCacheSizeLimit(GetCacheSizeLimitParams) returns (CacheSizeLimit); + rpc SetCacheSizeLimit(SetCacheSizeLimitParams) returns (google.protobuf.Empty); + rpc DeleteExpiredItems(DeleteExpiredItemsParams) returns (google.protobuf.Empty); + rpc DeleteUnlockedItems(DeleteUnlockedItemsParams) returns (google.protobuf.Empty); + rpc GetStats(GetStatsParams) returns (Stats); + rpc GetFileRanges(GetFileRangesParams) returns (FileRanges); +} + +message CacheSizeLimit { + int64 size = 1; +} + +message GetCacheSizeLimitParams { + +} + +message SetCacheSizeLimitParams { + CacheSizeLimit limit = 1; +} + +message DeleteExpiredItemsParams { + +} + +message DeleteUnlockedItemsParams { + +} + +message RealmStats { + Realm realm = 1; + int64 size = 2; + int64 num_entries = 3; + int64 num_complete_entries = 4; +} + +message Stats { + string cache_id = 1; + int64 creation_date_sec = 2; + int64 max_cache_size = 3; + int64 current_size = 4; + int64 current_locked_size = 5; + int64 free_space = 6; + int64 total_space = 7; + int64 current_numfiles = 8; + repeated RealmStats realm_stats = 9; +} + +message GetStatsParams { + +} + +message FileRanges { + bool byte_size_known = 1; + uint64 byte_size = 2; + + repeated Range ranges = 3; + message Range { + uint64 from_byte = 1; + uint64 to_byte = 2; + } +} + +message GetFileRangesParams { + Realm realm = 1; + string file_id = 2; +} + +enum Realm { + STREAM = 0; + COVER_ART = 1; + PLAYLIST = 4; + AUDIO_SHOW = 5; + HEAD_FILES = 7; + EXTERNAL_AUDIO_SHOW = 8; + KARAOKE_MASK = 9; +} diff --git a/protocol/proto/event_entity.proto b/protocol/proto/event_entity.proto index 28ec0b5a..06239d59 100644 --- a/protocol/proto/event_entity.proto +++ b/protocol/proto/event_entity.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -7,12 +7,12 @@ package spotify.event_sender.proto; option optimize_for = CODE_SIZE; message EventEntity { - int32 file_format_version = 1; + uint32 file_format_version = 1; string event_name = 2; bytes sequence_id = 3; - int64 sequence_number = 4; + uint64 sequence_number = 4; bytes payload = 5; string owner = 6; bool authenticated = 7; - int64 record_id = 8; + uint64 record_id = 8; } diff --git a/protocol/proto/extension_descriptor_type.proto b/protocol/proto/extension_descriptor_type.proto index a2009d68..2ca05713 100644 --- a/protocol/proto/extension_descriptor_type.proto +++ b/protocol/proto/extension_descriptor_type.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -26,4 +26,5 @@ enum ExtensionDescriptorType { INSTRUMENT = 4; TIME = 5; ERA = 6; + AESTHETIC = 7; } diff --git a/protocol/proto/extension_kind.proto b/protocol/proto/extension_kind.proto index 97768b67..02444dea 100644 --- a/protocol/proto/extension_kind.proto +++ b/protocol/proto/extension_kind.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.extendedmetadata; +option objc_class_prefix = "SPTExtendedMetadata"; option cc_enable_arenas = true; option java_multiple_files = true; option optimize_for = CODE_SIZE; @@ -43,4 +44,11 @@ enum ExtensionKind { SHOW_ACCESS = 31; PODCAST_QNA = 32; CLIPS = 33; + PODCAST_CTA_CARDS = 36; + PODCAST_RATING = 37; + DISPLAY_SEGMENTS = 38; + GREENROOM = 39; + USER_CREATED = 40; + CLIENT_CONFIG = 48; + AUDIOBOOK_SPECIFICS = 52; } diff --git a/protocol/proto/follow_request.proto b/protocol/proto/follow_request.proto new file mode 100644 index 00000000..5a026895 --- /dev/null +++ b/protocol/proto/follow_request.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.socialgraph_esperanto.proto; + +import "socialgraph_response_status.proto"; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.socialgraph.esperanto.proto"; + +message FollowRequest { + repeated string username = 1; + bool follow = 2; +} + +message FollowResponse { + ResponseStatus status = 1; +} diff --git a/protocol/proto/followed_users_request.proto b/protocol/proto/followed_users_request.proto new file mode 100644 index 00000000..afb71f43 --- /dev/null +++ b/protocol/proto/followed_users_request.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.socialgraph_esperanto.proto; + +import "socialgraph_response_status.proto"; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.socialgraph.esperanto.proto"; + +message FollowedUsersRequest { + bool force_reload = 1; +} + +message FollowedUsersResponse { + ResponseStatus status = 1; + repeated string users = 2; +} diff --git a/protocol/proto/google/protobuf/descriptor.proto b/protocol/proto/google/protobuf/descriptor.proto index 7f91c408..884a5151 100644 --- a/protocol/proto/google/protobuf/descriptor.proto +++ b/protocol/proto/google/protobuf/descriptor.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -189,7 +189,7 @@ message MessageOptions { extensions 1000 to max; - reserved 8, 9; + reserved 4, 5, 6, 8, 9; } message FieldOptions { diff --git a/protocol/proto/google/protobuf/empty.proto b/protocol/proto/google/protobuf/empty.proto new file mode 100644 index 00000000..28c4d77b --- /dev/null +++ b/protocol/proto/google/protobuf/empty.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option objc_class_prefix = "GPB"; +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/emptypb"; +option java_multiple_files = true; +option java_outer_classname = "EmptyProto"; +option java_package = "com.google.protobuf"; + +message Empty { + +} diff --git a/protocol/proto/greenroom_extension.proto b/protocol/proto/greenroom_extension.proto new file mode 100644 index 00000000..4fc8dbe3 --- /dev/null +++ b/protocol/proto/greenroom_extension.proto @@ -0,0 +1,29 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.greenroom.api.extendedmetadata.v1; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "GreenroomMetadataProto"; +option java_package = "com.spotify.greenroom.api.extendedmetadata.v1.proto"; + +message GreenroomSection { + repeated GreenroomItem items = 1; +} + +message GreenroomItem { + string title = 1; + string description = 2; + repeated GreenroomHost hosts = 3; + int64 start_timestamp = 4; + string deeplink_url = 5; + bool live = 6; +} + +message GreenroomHost { + string name = 1; + string image_url = 2; +} diff --git a/protocol/proto/format.proto b/protocol/proto/media_format.proto similarity index 84% rename from protocol/proto/format.proto rename to protocol/proto/media_format.proto index 3a75b4df..c54f6323 100644 --- a/protocol/proto/format.proto +++ b/protocol/proto/media_format.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -7,7 +7,7 @@ package spotify.stream_reporting_esperanto.proto; option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; -enum Format { +enum MediaFormat { FORMAT_UNKNOWN = 0; FORMAT_OGG_VORBIS_96 = 1; FORMAT_OGG_VORBIS_160 = 2; @@ -27,4 +27,6 @@ enum Format { FORMAT_MP4_256_CBCS = 16; FORMAT_FLAC_FLAC = 17; FORMAT_MP4_FLAC = 18; + FORMAT_MP4_Unknown = 19; + FORMAT_MP3_Unknown = 20; } diff --git a/protocol/proto/media_manifest.proto b/protocol/proto/media_manifest.proto index a6a97681..6e280259 100644 --- a/protocol/proto/media_manifest.proto +++ b/protocol/proto/media_manifest.proto @@ -1,8 +1,8 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; -package spotify.media_manifest; +package spotify.media_manifest.proto; option optimize_for = CODE_SIZE; @@ -33,9 +33,12 @@ message File { message ExternalFile { string method = 1; - string url = 2; - bytes body = 3; - bool is_webgate_endpoint = 4; + bytes body = 4; + + oneof endpoint { + string url = 2; + string service = 3; + } } message FileIdFile { diff --git a/protocol/proto/media_type.proto b/protocol/proto/media_type.proto index 5527922f..2d8def46 100644 --- a/protocol/proto/media_type.proto +++ b/protocol/proto/media_type.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -8,7 +8,6 @@ option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; enum MediaType { - MEDIA_TYPE_UNSET = 0; - AUDIO = 1; - VIDEO = 2; + AUDIO = 0; + VIDEO = 1; } diff --git a/protocol/proto/members_request.proto b/protocol/proto/members_request.proto new file mode 100644 index 00000000..931f91d3 --- /dev/null +++ b/protocol/proto/members_request.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.playlist.cosmos.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message OptionalLimit { + uint32 value = 1; +} + +message PlaylistMembersRequest { + string uri = 1; + OptionalLimit limit = 2; +} diff --git a/protocol/proto/members_response.proto b/protocol/proto/members_response.proto new file mode 100644 index 00000000..f341a8d2 --- /dev/null +++ b/protocol/proto/members_response.proto @@ -0,0 +1,35 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +import "playlist_permission.proto"; +import "playlist_user_state.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message Member { + optional User user = 1; + optional bool is_owner = 2; + optional uint32 num_tracks = 3; + optional uint32 num_episodes = 4; + optional FollowState follow_state = 5; + optional playlist_permission.proto.PermissionLevel permission_level = 6; +} + +message PlaylistMembersResponse { + optional string title = 1; + optional uint32 num_total_members = 2; + optional playlist_permission.proto.Capabilities capabilities = 3; + optional playlist_permission.proto.PermissionLevel base_permission_level = 4; + repeated Member members = 5; +} + +enum FollowState { + NONE = 0; + CAN_BE_FOLLOWED = 1; + CAN_BE_UNFOLLOWED = 2; +} diff --git a/protocol/proto/messages/discovery/force_discover.proto b/protocol/proto/messages/discovery/force_discover.proto new file mode 100644 index 00000000..22bcb066 --- /dev/null +++ b/protocol/proto/messages/discovery/force_discover.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.connect.esperanto.proto; + +option java_package = "com.spotify.connect.esperanto.proto"; + +message ForceDiscoverRequest { + +} + +message ForceDiscoverResponse { + +} diff --git a/protocol/proto/messages/discovery/start_discovery.proto b/protocol/proto/messages/discovery/start_discovery.proto new file mode 100644 index 00000000..d4af9339 --- /dev/null +++ b/protocol/proto/messages/discovery/start_discovery.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.connect.esperanto.proto; + +option java_package = "com.spotify.connect.esperanto.proto"; + +message StartDiscoveryRequest { + +} + +message StartDiscoveryResponse { + +} diff --git a/protocol/proto/metadata.proto b/protocol/proto/metadata.proto index a6d3aded..056dbcfa 100644 --- a/protocol/proto/metadata.proto +++ b/protocol/proto/metadata.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -140,6 +140,7 @@ message Show { repeated Availability availability = 78; optional string trailer_uri = 83; optional bool music_and_talk = 85; + optional bool is_audiobook = 89; } message Episode { @@ -173,6 +174,8 @@ message Episode { } optional bool music_and_talk = 91; + repeated ContentRating content_rating = 95; + optional bool is_audiobook_chapter = 96; } message Licensor { diff --git a/protocol/proto/metadata/episode_metadata.proto b/protocol/proto/metadata/episode_metadata.proto index 9f47deee..5d4a0b25 100644 --- a/protocol/proto/metadata/episode_metadata.proto +++ b/protocol/proto/metadata/episode_metadata.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.cosmos_util.proto; +import "metadata/extension.proto"; import "metadata/image_group.proto"; import "podcast_segments.proto"; import "podcast_subscription.proto"; @@ -56,4 +57,7 @@ message EpisodeMetadata { optional bool is_music_and_talk = 19; optional podcast_segments.PodcastSegments podcast_segments = 20; optional podcast_paywalls.PodcastSubscription podcast_subscription = 21; + repeated Extension extension = 22; + optional bool is_19_plus_only = 23; + optional bool is_book_chapter = 24; } diff --git a/protocol/proto/metadata/extension.proto b/protocol/proto/metadata/extension.proto new file mode 100644 index 00000000..b10a0f08 --- /dev/null +++ b/protocol/proto/metadata/extension.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +import "extension_kind.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message Extension { + optional extendedmetadata.ExtensionKind extension_kind = 1; + optional bytes data = 2; +} diff --git a/protocol/proto/metadata/show_metadata.proto b/protocol/proto/metadata/show_metadata.proto index 8beaf97b..9b9891d3 100644 --- a/protocol/proto/metadata/show_metadata.proto +++ b/protocol/proto/metadata/show_metadata.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.cosmos_util.proto; +import "metadata/extension.proto"; import "metadata/image_group.proto"; option java_multiple_files = true; @@ -25,4 +26,6 @@ message ShowMetadata { repeated string copyright = 12; optional string trailer_uri = 13; optional bool is_music_and_talk = 14; + repeated Extension extension = 15; + optional bool is_book = 16; } diff --git a/protocol/proto/metadata_esperanto.proto b/protocol/proto/metadata_esperanto.proto new file mode 100644 index 00000000..601290a1 --- /dev/null +++ b/protocol/proto/metadata_esperanto.proto @@ -0,0 +1,24 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.metadata_esperanto.proto; + +import "metadata_cosmos.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.metadata.esperanto.proto"; + +service ClassicMetadataService { + rpc GetEntity(GetEntityRequest) returns (GetEntityResponse); + rpc MultigetEntity(metadata_cosmos.proto.MultiRequest) returns (metadata_cosmos.proto.MultiResponse); +} + +message GetEntityRequest { + string uri = 1; +} + +message GetEntityResponse { + metadata_cosmos.proto.MetadataItem item = 1; +} diff --git a/protocol/proto/mod.rs b/protocol/proto/mod.rs index 9dfc8c92..24cf4052 100644 --- a/protocol/proto/mod.rs +++ b/protocol/proto/mod.rs @@ -1,4 +1,2 @@ // generated protobuf files will be included here. See build.rs for details -#![allow(renamed_and_removed_lints)] - include!(env!("PROTO_MOD_RS")); diff --git a/protocol/proto/offline_playlists_containing.proto b/protocol/proto/offline_playlists_containing.proto index 19106b0c..3d75865f 100644 --- a/protocol/proto/offline_playlists_containing.proto +++ b/protocol/proto/offline_playlists_containing.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.playlist.cosmos.proto; +option objc_class_prefix = "SPTPlaylist"; option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "com.spotify.playlist.proto"; diff --git a/protocol/proto/on_demand_set_cosmos_request.proto b/protocol/proto/on_demand_set_cosmos_request.proto index 28b70c16..72d4d3d9 100644 --- a/protocol/proto/on_demand_set_cosmos_request.proto +++ b/protocol/proto/on_demand_set_cosmos_request.proto @@ -1,10 +1,13 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.on_demand_set_cosmos.proto; +option objc_class_prefix = "SPT"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; +option java_package = "com.spotify.on_demand_set.proto"; message Set { repeated string uris = 1; diff --git a/protocol/proto/on_demand_set_cosmos_response.proto b/protocol/proto/on_demand_set_cosmos_response.proto index 3e5d708f..8ca68cbe 100644 --- a/protocol/proto/on_demand_set_cosmos_response.proto +++ b/protocol/proto/on_demand_set_cosmos_response.proto @@ -1,10 +1,13 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.on_demand_set_cosmos.proto; +option objc_class_prefix = "SPT"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; +option java_package = "com.spotify.on_demand_set.proto"; message Response { optional bool success = 1; diff --git a/protocol/proto/on_demand_set_response.proto b/protocol/proto/on_demand_set_response.proto new file mode 100644 index 00000000..9d914dd7 --- /dev/null +++ b/protocol/proto/on_demand_set_response.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.on_demand_set_esperanto.proto; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.on_demand_set.proto"; + +message ResponseStatus { + int32 status_code = 1; + string reason = 2; +} diff --git a/protocol/proto/pending_event_entity.proto b/protocol/proto/pending_event_entity.proto new file mode 100644 index 00000000..0dd5c099 --- /dev/null +++ b/protocol/proto/pending_event_entity.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.pending_events.proto; + +option optimize_for = CODE_SIZE; + +message PendingEventEntity { + string event_name = 1; + bytes payload = 2; + string username = 3; +} diff --git a/protocol/proto/perf_metrics_service.proto b/protocol/proto/perf_metrics_service.proto new file mode 100644 index 00000000..484bd321 --- /dev/null +++ b/protocol/proto/perf_metrics_service.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.perf_metrics.esperanto.proto; + +option java_package = "com.spotify.perf_metrics.esperanto.proto"; + +service PerfMetricsService { + rpc TerminateState(PerfMetricsRequest) returns (PerfMetricsResponse); +} + +message PerfMetricsRequest { + string terminal_state = 1; + bool foreground_startup = 2; +} + +message PerfMetricsResponse { + bool success = 1; +} diff --git a/protocol/proto/pin_request.proto b/protocol/proto/pin_request.proto index 23e064ad..a5337320 100644 --- a/protocol/proto/pin_request.proto +++ b/protocol/proto/pin_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -19,6 +19,7 @@ message PinResponse { } bool has_maximum_pinned_items = 2; + int32 maximum_pinned_items = 3; string error = 99; } diff --git a/protocol/proto/play_reason.proto b/protocol/proto/play_reason.proto index 6ebfc914..04bba83f 100644 --- a/protocol/proto/play_reason.proto +++ b/protocol/proto/play_reason.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -8,26 +8,25 @@ option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; enum PlayReason { - REASON_UNSET = 0; - REASON_APP_LOAD = 1; - REASON_BACK_BTN = 2; - REASON_CLICK_ROW = 3; - REASON_CLICK_SIDE = 4; - REASON_END_PLAY = 5; - REASON_FWD_BTN = 6; - REASON_INTERRUPTED = 7; - REASON_LOGOUT = 8; - REASON_PLAY_BTN = 9; - REASON_POPUP = 10; - REASON_REMOTE = 11; - REASON_SONG_DONE = 12; - REASON_TRACK_DONE = 13; - REASON_TRACK_ERROR = 14; - REASON_PREVIEW = 15; - REASON_PLAY_REASON_UNKNOWN = 16; - REASON_URI_OPEN = 17; - REASON_BACKGROUNDED = 18; - REASON_OFFLINE = 19; - REASON_UNEXPECTED_EXIT = 20; - REASON_UNEXPECTED_EXIT_WHILE_PAUSED = 21; + PLAY_REASON_UNKNOWN = 0; + PLAY_REASON_APP_LOAD = 1; + PLAY_REASON_BACK_BTN = 2; + PLAY_REASON_CLICK_ROW = 3; + PLAY_REASON_CLICK_SIDE = 4; + PLAY_REASON_END_PLAY = 5; + PLAY_REASON_FWD_BTN = 6; + PLAY_REASON_INTERRUPTED = 7; + PLAY_REASON_LOGOUT = 8; + PLAY_REASON_PLAY_BTN = 9; + PLAY_REASON_POPUP = 10; + PLAY_REASON_REMOTE = 11; + PLAY_REASON_SONG_DONE = 12; + PLAY_REASON_TRACK_DONE = 13; + PLAY_REASON_TRACK_ERROR = 14; + PLAY_REASON_PREVIEW = 15; + PLAY_REASON_URI_OPEN = 16; + PLAY_REASON_BACKGROUNDED = 17; + PLAY_REASON_OFFLINE = 18; + PLAY_REASON_UNEXPECTED_EXIT = 19; + PLAY_REASON_UNEXPECTED_EXIT_WHILE_PAUSED = 20; } diff --git a/protocol/proto/play_source.proto b/protocol/proto/play_source.proto deleted file mode 100644 index e4db2b9a..00000000 --- a/protocol/proto/play_source.proto +++ /dev/null @@ -1,47 +0,0 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) - -syntax = "proto3"; - -package spotify.stream_reporting_esperanto.proto; - -option objc_class_prefix = "ESP"; -option java_package = "com.spotify.stream_reporting_esperanto.proto"; - -enum PlaySource { - SOURCE_UNSET = 0; - SOURCE_ALBUM = 1; - SOURCE_ARTIST = 2; - SOURCE_ARTIST_RADIO = 3; - SOURCE_COLLECTION = 4; - SOURCE_DEVICE_SECTION = 5; - SOURCE_EXTERNAL_DEVICE = 6; - SOURCE_EXT_LINK = 7; - SOURCE_INBOX = 8; - SOURCE_LIBRARY = 9; - SOURCE_LIBRARY_COLLECTION = 10; - SOURCE_LIBRARY_COLLECTION_ALBUM = 11; - SOURCE_LIBRARY_COLLECTION_ARTIST = 12; - SOURCE_LIBRARY_COLLECTION_MISSING_ALBUM = 13; - SOURCE_LOCAL_FILES = 14; - SOURCE_PENDAD = 15; - SOURCE_PLAYLIST = 16; - SOURCE_PLAYLIST_OWNED_BY_OTHER_COLLABORATIVE = 17; - SOURCE_PLAYLIST_OWNED_BY_OTHER_NON_COLLABORATIVE = 18; - SOURCE_PLAYLIST_OWNED_BY_SELF_COLLABORATIVE = 19; - SOURCE_PLAYLIST_OWNED_BY_SELF_NON_COLLABORATIVE = 20; - SOURCE_PLAYLIST_FOLDER = 21; - SOURCE_PLAYLISTS = 22; - SOURCE_PLAY_QUEUE = 23; - SOURCE_PLUGIN_API = 24; - SOURCE_PROFILE = 25; - SOURCE_PURCHASES = 26; - SOURCE_RADIO = 27; - SOURCE_RTMP = 28; - SOURCE_SEARCH = 29; - SOURCE_SHOW = 30; - SOURCE_TEMP_PLAYLIST = 31; - SOURCE_TOPLIST = 32; - SOURCE_TRACK_SET = 33; - SOURCE_PLAY_SOURCE_UNKNOWN = 34; - SOURCE_QUICK_MENU = 35; -} diff --git a/protocol/proto/playback_cosmos.proto b/protocol/proto/playback_cosmos.proto index 83a905fd..b2ae4f96 100644 --- a/protocol/proto/playback_cosmos.proto +++ b/protocol/proto/playback_cosmos.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -60,11 +60,12 @@ message InfoResponse { float gain_adjustment = 13; bool has_loudness = 14; float loudness = 15; - string file_origin = 16; string strategy = 17; int32 target_bitrate = 18; int32 advised_bitrate = 19; bool target_file_available = 20; + + reserved 16; } message FormatsResponse { diff --git a/protocol/proto/playback_esperanto.proto b/protocol/proto/playback_esperanto.proto new file mode 100644 index 00000000..3c57325a --- /dev/null +++ b/protocol/proto/playback_esperanto.proto @@ -0,0 +1,122 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.playback_esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playback_esperanto.proto"; + +message GetVolumeResponse { + Status status = 1; + double volume = 2; +} + +message SubVolumeResponse { + Status status = 1; + double volume = 2; + VolumeChangeSource source = 3; +} + +message SetVolumeRequest { + VolumeChangeSource source = 1; + double volume = 2; +} + +message NudgeVolumeRequest { + VolumeChangeSource source = 1; +} + +message PlaybackInfoResponse { + Status status = 1; + uint64 length_ms = 2; + uint64 position_ms = 3; + bool playing = 4; + bool buffering = 5; + int32 error = 6; + string file_id = 7; + string file_type = 8; + string resolved_content_url = 9; + int32 file_bitrate = 10; + string codec_name = 11; + double playback_speed = 12; + float gain_adjustment = 13; + bool has_loudness = 14; + float loudness = 15; + string strategy = 17; + int32 target_bitrate = 18; + int32 advised_bitrate = 19; + bool target_file_available = 20; + + reserved 16; +} + +message GetFormatsResponse { + repeated Format formats = 1; + message Format { + string enum_key = 1; + uint32 enum_value = 2; + bool supported = 3; + uint32 bitrate = 4; + string mime_type = 5; + } +} + +message SubPositionRequest { + uint64 position = 1; +} + +message SubPositionResponse { + Status status = 1; + uint64 position = 2; +} + +message GetFilesRequest { + string uri = 1; +} + +message GetFilesResponse { + GetFilesStatus status = 1; + + repeated File files = 2; + message File { + string file_id = 1; + string format = 2; + uint32 bitrate = 3; + uint32 format_enum = 4; + } +} + +message DuckRequest { + Action action = 2; + enum Action { + START = 0; + STOP = 1; + } + + double volume = 3; + uint32 fade_duration_ms = 4; +} + +message DuckResponse { + Status status = 1; +} + +enum Status { + OK = 0; + NOT_AVAILABLE = 1; +} + +enum GetFilesStatus { + GETFILES_OK = 0; + METADATA_CLIENT_NOT_AVAILABLE = 1; + FILES_NOT_FOUND = 2; + TRACK_NOT_AVAILABLE = 3; + EXTENDED_METADATA_ERROR = 4; +} + +enum VolumeChangeSource { + USER = 0; + SYSTEM = 1; +} diff --git a/protocol/proto/playback_platform.proto b/protocol/proto/playback_platform.proto new file mode 100644 index 00000000..5f50bd95 --- /dev/null +++ b/protocol/proto/playback_platform.proto @@ -0,0 +1,90 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.playback_platform.proto; + +import "media_manifest.proto"; + +option optimize_for = CODE_SIZE; + +message Media { + string id = 1; + int32 start_position = 6; + int32 stop_position = 7; + + oneof source { + string audio_id = 2; + string episode_id = 3; + string track_id = 4; + media_manifest.proto.Files files = 5; + } +} + +message Annotation { + map metadata = 2; +} + +message PlaybackControl { + +} + +message Context { + string id = 2; + string type = 3; + + reserved 1; +} + +message Timeline { + repeated MediaTrack media_tracks = 1; + message MediaTrack { + repeated Item items = 1; + message Item { + repeated Annotation annotations = 3; + repeated PlaybackControl controls = 4; + + oneof content { + Context context = 1; + Media media = 2; + } + } + } +} + +message PageId { + Context context = 1; + int32 index = 2; +} + +message PagePath { + repeated PageId segments = 1; +} + +message Page { + Header header = 1; + message Header { + int32 status_code = 1; + int32 num_pages = 2; + } + + PageId page_id = 2; + Timeline timeline = 3; +} + +message PageList { + repeated Page pages = 1; +} + +message PageMultiGetRequest { + repeated PageId page_ids = 1; +} + +message PageMultiGetResponse { + repeated Page pages = 1; +} + +message ContextPagePathState { + PagePath path = 1; + repeated int32 media_track_item_index = 3; +} diff --git a/protocol/proto/played_state/show_played_state.proto b/protocol/proto/played_state/show_played_state.proto index 08910f93..47f13ec7 100644 --- a/protocol/proto/played_state/show_played_state.proto +++ b/protocol/proto/played_state/show_played_state.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.cosmos_util.proto; +option objc_class_prefix = "SPTCosmosUtil"; option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "com.spotify.cosmos.util.proto"; diff --git a/protocol/proto/playlist4_external.proto b/protocol/proto/playlist4_external.proto index 0a5d7084..2a7b44b9 100644 --- a/protocol/proto/playlist4_external.proto +++ b/protocol/proto/playlist4_external.proto @@ -1,9 +1,11 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.playlist4.proto; +import "playlist_permission.proto"; + option optimize_for = CODE_SIZE; option java_outer_classname = "Playlist4ApiProto"; option java_package = "com.spotify.playlist4.proto"; @@ -19,6 +21,8 @@ message MetaItem { optional int32 length = 3; optional int64 timestamp = 4; optional string owner_username = 5; + optional bool abuse_reporting_enabled = 6; + optional spotify.playlist_permission.proto.Capabilities capabilities = 7; } message ListItems { @@ -187,16 +191,45 @@ message SelectedListContent { optional int64 timestamp = 15; optional string owner_username = 16; optional bool abuse_reporting_enabled = 17; + optional spotify.playlist_permission.proto.Capabilities capabilities = 18; + repeated GeoblockBlockingType geoblock = 19; } message CreateListReply { - required bytes uri = 1; + required string uri = 1; optional bytes revision = 2; } -message ModifyReply { - required bytes uri = 1; - optional bytes revision = 2; +message PlaylistV1UriRequest { + repeated string v2_uris = 1; +} + +message PlaylistV1UriReply { + map v2_uri_to_v1_uri = 1; +} + +message ListUpdateRequest { + optional bytes base_revision = 1; + optional ListAttributes attributes = 2; + repeated Item items = 3; + optional ChangeInfo info = 4; +} + +message RegisterPlaylistImageRequest { + optional string upload_token = 1; +} + +message RegisterPlaylistImageResponse { + optional bytes picture = 1; +} + +message ResolvedPersonalizedPlaylist { + optional string uri = 1; + optional string tag = 2; +} + +message PlaylistUriResolverResponse { + repeated ResolvedPersonalizedPlaylist resolved_playlists = 1; } message SubscribeRequest { @@ -214,6 +247,19 @@ message PlaylistModificationInfo { repeated Op ops = 4; } +message RootlistModificationInfo { + optional bytes new_revision = 1; + optional bytes parent_revision = 2; + repeated Op ops = 3; +} + +message FollowerUpdate { + optional string uri = 1; + optional string username = 2; + optional bool is_following = 3; + optional uint64 timestamp = 4; +} + enum ListAttributeKind { LIST_UNKNOWN = 0; LIST_NAME = 1; @@ -237,3 +283,10 @@ enum ItemAttributeKind { ITEM_FORMAT_ATTRIBUTES = 11; ITEM_ID = 12; } + +enum GeoblockBlockingType { + GEOBLOCK_BLOCKING_TYPE_UNSPECIFIED = 0; + GEOBLOCK_BLOCKING_TYPE_TITLE = 1; + GEOBLOCK_BLOCKING_TYPE_DESCRIPTION = 2; + GEOBLOCK_BLOCKING_TYPE_IMAGE = 3; +} diff --git a/protocol/proto/playlist_contains_request.proto b/protocol/proto/playlist_contains_request.proto new file mode 100644 index 00000000..072d5379 --- /dev/null +++ b/protocol/proto/playlist_contains_request.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.playlist_esperanto.proto; + +import "contains_request.proto"; +import "response_status.proto"; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.playlist.esperanto.proto"; + +message PlaylistContainsRequest { + string uri = 1; + playlist.cosmos.proto.ContainsRequest request = 2; +} + +message PlaylistContainsResponse { + ResponseStatus status = 1; + playlist.cosmos.proto.ContainsResponse response = 2; +} diff --git a/protocol/proto/playlist_members_request.proto b/protocol/proto/playlist_members_request.proto new file mode 100644 index 00000000..d5bd9b98 --- /dev/null +++ b/protocol/proto/playlist_members_request.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.playlist_esperanto.proto; + +import "members_request.proto"; +import "members_response.proto"; +import "response_status.proto"; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.playlist.esperanto.proto"; + +message PlaylistMembersResponse { + ResponseStatus status = 1; + playlist.cosmos.proto.PlaylistMembersResponse response = 2; +} diff --git a/protocol/proto/playlist_offline_request.proto b/protocol/proto/playlist_offline_request.proto new file mode 100644 index 00000000..e0ab6312 --- /dev/null +++ b/protocol/proto/playlist_offline_request.proto @@ -0,0 +1,29 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.playlist_esperanto.proto; + +import "playlist_query.proto"; +import "response_status.proto"; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.playlist.esperanto.proto"; + +message PlaylistOfflineRequest { + string uri = 1; + PlaylistQuery query = 2; + PlaylistOfflineAction action = 3; +} + +message PlaylistOfflineResponse { + ResponseStatus status = 1; +} + +enum PlaylistOfflineAction { + NONE = 0; + SET_AS_AVAILABLE_OFFLINE = 1; + REMOVE_AS_AVAILABLE_OFFLINE = 2; +} diff --git a/protocol/proto/playlist_permission.proto b/protocol/proto/playlist_permission.proto index babab040..96e9c06d 100644 --- a/protocol/proto/playlist_permission.proto +++ b/protocol/proto/playlist_permission.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -19,6 +19,7 @@ message Capabilities { repeated PermissionLevel grantable_level = 3; optional bool can_edit_metadata = 4; optional bool can_edit_items = 5; + optional bool can_cancel_membership = 6; } message CapabilitiesMultiRequest { @@ -52,6 +53,10 @@ message SetPermissionResponse { optional Permission resulting_permission = 1; } +message GetMemberPermissionsResponse { + map member_permissions = 1; +} + message Permissions { optional Permission base_permission = 1; } @@ -67,6 +72,21 @@ message PermissionStatePub { optional PermissionState permission_state = 1; } +message PermissionGrantOptions { + optional Permission permission = 1; + optional int64 ttl_ms = 2; +} + +message PermissionGrant { + optional string token = 1; + optional PermissionGrantOptions permission_grant_options = 2; +} + +message ClaimPermissionGrantResponse { + optional Permission user_permission = 1; + optional Capabilities capabilities = 2; +} + message ResponseStatus { optional int32 status_code = 1; optional string status_message = 2; diff --git a/protocol/proto/playlist_playlist_state.proto b/protocol/proto/playlist_playlist_state.proto index 4356fe65..5663252c 100644 --- a/protocol/proto/playlist_playlist_state.proto +++ b/protocol/proto/playlist_playlist_state.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.playlist.cosmos.proto; +import "metadata/extension.proto"; import "metadata/image_group.proto"; import "playlist_user_state.proto"; @@ -42,6 +43,7 @@ message PlaylistMetadata { optional Allows allows = 18; optional string load_state = 19; optional User made_for = 20; + repeated cosmos_util.proto.Extension extension = 21; } message PlaylistOfflineState { diff --git a/protocol/proto/playlist_request.proto b/protocol/proto/playlist_request.proto index cb452f63..52befb1f 100644 --- a/protocol/proto/playlist_request.proto +++ b/protocol/proto/playlist_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -17,6 +17,7 @@ import "playlist_track_state.proto"; import "playlist_user_state.proto"; import "metadata/track_metadata.proto"; +option objc_class_prefix = "SPTPlaylistCosmosPlaylist"; option optimize_for = CODE_SIZE; option java_package = "com.spotify.playlist.proto"; @@ -86,4 +87,5 @@ message Response { optional on_demand_set.proto.OnDemandInFreeReason on_demand_in_free_reason = 21; optional Collaborators collaborators = 22; optional playlist_permission.proto.Permission base_permission = 23; + optional playlist_permission.proto.Capabilities user_capabilities = 24; } diff --git a/protocol/proto/playlist_set_member_permission_request.proto b/protocol/proto/playlist_set_member_permission_request.proto new file mode 100644 index 00000000..d3d687a4 --- /dev/null +++ b/protocol/proto/playlist_set_member_permission_request.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.playlist_esperanto.proto; + +import "response_status.proto"; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.playlist.esperanto.proto"; + +message PlaylistSetMemberPermissionResponse { + ResponseStatus status = 1; +} diff --git a/protocol/proto/playlist_track_state.proto b/protocol/proto/playlist_track_state.proto index 5bd64ae2..cd55947f 100644 --- a/protocol/proto/playlist_track_state.proto +++ b/protocol/proto/playlist_track_state.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.playlist.cosmos.proto; +option objc_class_prefix = "SPTPlaylist"; option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "com.spotify.playlist.proto"; diff --git a/protocol/proto/playlist_user_state.proto b/protocol/proto/playlist_user_state.proto index 510630ca..86c07dee 100644 --- a/protocol/proto/playlist_user_state.proto +++ b/protocol/proto/playlist_user_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -14,4 +14,5 @@ message User { optional string display_name = 3; optional string image_uri = 4; optional string thumbnail_uri = 5; + optional int32 color = 6; } diff --git a/protocol/proto/playlist_v1_uri.proto b/protocol/proto/playlist_v1_uri.proto deleted file mode 100644 index 76c9d797..00000000 --- a/protocol/proto/playlist_v1_uri.proto +++ /dev/null @@ -1,15 +0,0 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - -syntax = "proto2"; - -package spotify.player.proto; - -option optimize_for = CODE_SIZE; - -message PlaylistV1UriRequest { - repeated string v2_uris = 1; -} - -message PlaylistV1UriReply { - map v2_uri_to_v1_uri = 1; -} diff --git a/protocol/proto/podcast_cta_cards.proto b/protocol/proto/podcast_cta_cards.proto new file mode 100644 index 00000000..9cd4dfc6 --- /dev/null +++ b/protocol/proto/podcast_cta_cards.proto @@ -0,0 +1,9 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.context_track_exts.podcastctacards; + +message Card { + bool has_cards = 1; +} diff --git a/protocol/proto/podcast_ratings.proto b/protocol/proto/podcast_ratings.proto new file mode 100644 index 00000000..c78c0282 --- /dev/null +++ b/protocol/proto/podcast_ratings.proto @@ -0,0 +1,32 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.ratings; + +import "google/protobuf/timestamp.proto"; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "RatingsMetadataProto"; +option java_package = "com.spotify.podcastcreatorinteractivity.v1"; + +message Rating { + string user_id = 1; + string show_uri = 2; + int32 rating = 3; + google.protobuf.Timestamp rated_at = 4; +} + +message AverageRating { + double average = 1; + int64 total_ratings = 2; + bool show_average = 3; +} + +message PodcastRating { + AverageRating average_rating = 1; + Rating rating = 2; + bool can_rate = 3; +} diff --git a/protocol/proto/policy/album_decoration_policy.proto b/protocol/proto/policy/album_decoration_policy.proto index a20cf324..359347d4 100644 --- a/protocol/proto/policy/album_decoration_policy.proto +++ b/protocol/proto/policy/album_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -19,3 +19,15 @@ message AlbumDecorationPolicy { bool playability = 8; bool is_premium_only = 9; } + +message AlbumCollectionDecorationPolicy { + bool collection_link = 1; + bool num_tracks_in_collection = 2; + bool complete = 3; +} + +message AlbumSyncDecorationPolicy { + bool inferred_offline = 1; + bool offline_state = 2; + bool sync_progress = 3; +} diff --git a/protocol/proto/policy/artist_decoration_policy.proto b/protocol/proto/policy/artist_decoration_policy.proto index f8d8b2cb..0419dc31 100644 --- a/protocol/proto/policy/artist_decoration_policy.proto +++ b/protocol/proto/policy/artist_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -14,3 +14,18 @@ message ArtistDecorationPolicy { bool is_various_artists = 3; bool portraits = 4; } + +message ArtistCollectionDecorationPolicy { + bool collection_link = 1; + bool is_followed = 2; + bool num_tracks_in_collection = 3; + bool num_albums_in_collection = 4; + bool is_banned = 5; + bool can_ban = 6; +} + +message ArtistSyncDecorationPolicy { + bool inferred_offline = 1; + bool offline_state = 2; + bool sync_progress = 3; +} diff --git a/protocol/proto/policy/episode_decoration_policy.proto b/protocol/proto/policy/episode_decoration_policy.proto index 77489834..467426bd 100644 --- a/protocol/proto/policy/episode_decoration_policy.proto +++ b/protocol/proto/policy/episode_decoration_policy.proto @@ -1,9 +1,11 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.cosmos_util.proto; +import "extension_kind.proto"; + option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "com.spotify.cosmos.util.policy.proto"; @@ -29,6 +31,9 @@ message EpisodeDecorationPolicy { bool is_music_and_talk = 18; PodcastSegmentsPolicy podcast_segments = 19; bool podcast_subscription = 20; + repeated extendedmetadata.ExtensionKind extension = 21; + bool is_19_plus_only = 22; + bool is_book_chapter = 23; } message EpisodeCollectionDecorationPolicy { @@ -47,6 +52,7 @@ message EpisodePlayedStateDecorationPolicy { bool is_played = 2; bool playable = 3; bool playability_restriction = 4; + bool last_played_at = 5; } message PodcastSegmentsPolicy { diff --git a/protocol/proto/policy/playlist_decoration_policy.proto b/protocol/proto/policy/playlist_decoration_policy.proto index 9975279c..a6aef1b7 100644 --- a/protocol/proto/policy/playlist_decoration_policy.proto +++ b/protocol/proto/policy/playlist_decoration_policy.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.playlist.cosmos.proto; +import "extension_kind.proto"; import "policy/user_decoration_policy.proto"; option java_multiple_files = true; @@ -57,4 +58,6 @@ message PlaylistDecorationPolicy { bool on_demand_in_free_reason = 39; CollaboratingUsersDecorationPolicy collaborating_users = 40; bool base_permission = 41; + bool user_capabilities = 42; + repeated extendedmetadata.ExtensionKind extension = 43; } diff --git a/protocol/proto/policy/show_decoration_policy.proto b/protocol/proto/policy/show_decoration_policy.proto index 02ae2f3e..2e5e2020 100644 --- a/protocol/proto/policy/show_decoration_policy.proto +++ b/protocol/proto/policy/show_decoration_policy.proto @@ -1,9 +1,11 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.cosmos_util.proto; +import "extension_kind.proto"; + option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "com.spotify.cosmos.util.policy.proto"; @@ -24,6 +26,8 @@ message ShowDecorationPolicy { bool trailer_uri = 13; bool is_music_and_talk = 14; bool access_info = 15; + repeated extendedmetadata.ExtensionKind extension = 16; + bool is_book = 17; } message ShowPlayedStateDecorationPolicy { diff --git a/protocol/proto/policy/track_decoration_policy.proto b/protocol/proto/policy/track_decoration_policy.proto index 45162008..aa71f497 100644 --- a/protocol/proto/policy/track_decoration_policy.proto +++ b/protocol/proto/policy/track_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -34,3 +34,15 @@ message TrackPlayedStateDecorationPolicy { bool is_currently_playable = 2; bool playability_restriction = 3; } + +message TrackCollectionDecorationPolicy { + bool is_in_collection = 1; + bool can_add_to_collection = 2; + bool is_banned = 3; + bool can_ban = 4; +} + +message TrackSyncDecorationPolicy { + bool offline_state = 1; + bool sync_progress = 2; +} diff --git a/protocol/proto/policy/user_decoration_policy.proto b/protocol/proto/policy/user_decoration_policy.proto index 4f72e974..f2c342eb 100644 --- a/protocol/proto/policy/user_decoration_policy.proto +++ b/protocol/proto/policy/user_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -14,6 +14,7 @@ message UserDecorationPolicy { bool name = 3; bool image = 4; bool thumbnail = 5; + bool color = 6; } message CollaboratorPolicy { diff --git a/protocol/proto/prepare_play_options.proto b/protocol/proto/prepare_play_options.proto index cfaeab14..cb27650d 100644 --- a/protocol/proto/prepare_play_options.proto +++ b/protocol/proto/prepare_play_options.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -6,6 +6,7 @@ package spotify.player.proto; import "context_player_options.proto"; import "player_license.proto"; +import "skip_to_track.proto"; option optimize_for = CODE_SIZE; @@ -13,4 +14,24 @@ message PreparePlayOptions { optional ContextPlayerOptionOverrides player_options_override = 1; optional PlayerLicense license = 2; map configuration_override = 3; + optional string playback_id = 4; + optional bool always_play_something = 5; + optional SkipToTrack skip_to_track = 6; + optional int64 seek_to = 7; + optional bool initially_paused = 8; + optional bool system_initiated = 9; + repeated string suppressions = 10; + optional PrefetchLevel prefetch_level = 11; + optional string session_id = 12; + optional AudioStream audio_stream = 13; +} + +enum PrefetchLevel { + kNone = 0; + kMedia = 1; +} + +enum AudioStream { + kDefault = 0; + kAlarm = 1; } diff --git a/protocol/proto/profile_cache.proto b/protocol/proto/profile_cache.proto deleted file mode 100644 index 8162612f..00000000 --- a/protocol/proto/profile_cache.proto +++ /dev/null @@ -1,19 +0,0 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - -syntax = "proto3"; - -package spotify.profile.proto; - -import "identity.proto"; - -option optimize_for = CODE_SIZE; - -message CachedProfile { - identity.proto.DecorationData profile = 1; - int64 expires_at = 2; - bool pinned = 3; -} - -message ProfileCacheFile { - repeated CachedProfile cached_profiles = 1; -} diff --git a/protocol/proto/profile_service.proto b/protocol/proto/profile_service.proto new file mode 100644 index 00000000..194e5fea --- /dev/null +++ b/protocol/proto/profile_service.proto @@ -0,0 +1,33 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.profile_esperanto.proto.v1; + +import "identity.proto"; + +option optimize_for = CODE_SIZE; + +service ProfileService { + rpc GetProfiles(GetProfilesRequest) returns (GetProfilesResponse); + rpc SubscribeToProfiles(GetProfilesRequest) returns (stream GetProfilesResponse); + rpc ChangeDisplayName(ChangeDisplayNameRequest) returns (ChangeDisplayNameResponse); +} + +message GetProfilesRequest { + repeated string usernames = 1; +} + +message GetProfilesResponse { + repeated identity.v3.UserProfile profiles = 1; + int32 status_code = 2; +} + +message ChangeDisplayNameRequest { + string username = 1; + string display_name = 2; +} + +message ChangeDisplayNameResponse { + int32 status_code = 1; +} diff --git a/protocol/proto/property_definition.proto b/protocol/proto/property_definition.proto index 4552c1b2..9df7caa7 100644 --- a/protocol/proto/property_definition.proto +++ b/protocol/proto/property_definition.proto @@ -25,7 +25,7 @@ message PropertyDefinition { EnumSpec enum_spec = 7; } - reserved 2, "hash"; + //reserved 2, "hash"; message BoolSpec { bool default = 1; diff --git a/protocol/proto/rate_limited_events.proto b/protocol/proto/rate_limited_events.proto new file mode 100644 index 00000000..c9116b6d --- /dev/null +++ b/protocol/proto/rate_limited_events.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message RateLimitedEventsEntity { + int32 file_format_version = 1; + map map_field = 2; +} diff --git a/protocol/proto/rc_dummy_property_resolved.proto b/protocol/proto/rc_dummy_property_resolved.proto deleted file mode 100644 index 9c5e2aaf..00000000 --- a/protocol/proto/rc_dummy_property_resolved.proto +++ /dev/null @@ -1,12 +0,0 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - -syntax = "proto3"; - -package spotify.remote_config.proto; - -option optimize_for = CODE_SIZE; - -message RcDummyPropertyResolved { - string resolved_value = 1; - string configuration_assignment_id = 2; -} diff --git a/protocol/proto/rcs.proto b/protocol/proto/rcs.proto index ed8405c2..00e86103 100644 --- a/protocol/proto/rcs.proto +++ b/protocol/proto/rcs.proto @@ -52,7 +52,7 @@ message ClientPropertySet { message ComponentInfo { string name = 3; - reserved 1, 2, "owner", "tags"; + //reserved 1, 2, "owner", "tags"; } string property_set_key = 7; diff --git a/protocol/proto/record_id.proto b/protocol/proto/record_id.proto index 54fa24a3..167c0ecd 100644 --- a/protocol/proto/record_id.proto +++ b/protocol/proto/record_id.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -7,5 +7,5 @@ package spotify.event_sender.proto; option optimize_for = CODE_SIZE; message RecordId { - int64 value = 1; + uint64 value = 1; } diff --git a/protocol/proto/resolve.proto b/protocol/proto/resolve.proto index 5f2cd9b8..793b8c5a 100644 --- a/protocol/proto/resolve.proto +++ b/protocol/proto/resolve.proto @@ -17,7 +17,7 @@ message ResolveRequest { BackendContext backend_context = 12 [deprecated = true]; } - reserved 4, 5, "custom_context", "projection"; + //reserved 4, 5, "custom_context", "projection"; } message ResolveResponse { diff --git a/protocol/proto/resolve_configuration_error.proto b/protocol/proto/resolve_configuration_error.proto deleted file mode 100644 index 22f2e1fb..00000000 --- a/protocol/proto/resolve_configuration_error.proto +++ /dev/null @@ -1,14 +0,0 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - -syntax = "proto3"; - -package spotify.remote_config.proto; - -option optimize_for = CODE_SIZE; - -message ResolveConfigurationError { - string error_message = 1; - int64 status_code = 2; - string client_id = 3; - string client_version = 4; -} diff --git a/protocol/proto/response_status.proto b/protocol/proto/response_status.proto index a9ecadd7..5709571f 100644 --- a/protocol/proto/response_status.proto +++ b/protocol/proto/response_status.proto @@ -1,10 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.playlist_esperanto.proto; -option objc_class_prefix = "ESP"; +option objc_class_prefix = "SPTPlaylistEsperanto"; option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "spotify.playlist.esperanto.proto"; diff --git a/protocol/proto/rootlist_request.proto b/protocol/proto/rootlist_request.proto index 80af73f0..ae055475 100644 --- a/protocol/proto/rootlist_request.proto +++ b/protocol/proto/rootlist_request.proto @@ -1,13 +1,15 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.playlist.cosmos.rootlist_request.proto; import "playlist_folder_state.proto"; +import "playlist_permission.proto"; import "playlist_playlist_state.proto"; import "protobuf_delta.proto"; +option objc_class_prefix = "SPTPlaylistCosmosRootlist"; option optimize_for = CODE_SIZE; option java_package = "com.spotify.playlist.proto"; @@ -18,6 +20,7 @@ message Playlist { optional uint32 add_time = 4; optional bool is_on_demand_in_free = 5; optional string group_label = 6; + optional playlist_permission.proto.Capabilities capabilities = 7; } message Item { diff --git a/protocol/proto/sequence_number_entity.proto b/protocol/proto/sequence_number_entity.proto index cd97392c..a3b88c81 100644 --- a/protocol/proto/sequence_number_entity.proto +++ b/protocol/proto/sequence_number_entity.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -7,8 +7,8 @@ package spotify.event_sender.proto; option optimize_for = CODE_SIZE; message SequenceNumberEntity { - int32 file_format_version = 1; + uint32 file_format_version = 1; string event_name = 2; bytes sequence_id = 3; - int64 sequence_number_next = 4; + uint64 sequence_number_next = 4; } diff --git a/protocol/proto/set_member_permission_request.proto b/protocol/proto/set_member_permission_request.proto new file mode 100644 index 00000000..160eaf92 --- /dev/null +++ b/protocol/proto/set_member_permission_request.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +import "playlist_permission.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message SetMemberPermissionRequest { + optional string playlist_uri = 1; + optional string username = 2; + optional playlist_permission.proto.PermissionLevel permission_level = 3; + optional uint32 timeout_ms = 4; +} diff --git a/protocol/proto/show_access.proto b/protocol/proto/show_access.proto index 3516cdfd..eddc0342 100644 --- a/protocol/proto/show_access.proto +++ b/protocol/proto/show_access.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -11,10 +11,13 @@ option java_outer_classname = "ShowAccessProto"; option java_package = "com.spotify.podcast.access.proto"; message ShowAccess { + AccountLinkPrompt prompt = 5; + oneof explanation { NoExplanation none = 1; LegacyExplanation legacy = 2; BasicExplanation basic = 3; + UpsellLinkExplanation upsellLink = 4; } } @@ -31,3 +34,17 @@ message LegacyExplanation { message NoExplanation { } + +message UpsellLinkExplanation { + string title = 1; + string body = 2; + string cta = 3; + string url = 4; +} + +message AccountLinkPrompt { + string title = 1; + string body = 2; + string cta = 3; + string url = 4; +} diff --git a/protocol/proto/show_episode_state.proto b/protocol/proto/show_episode_state.proto index 001fafee..b780dbb6 100644 --- a/protocol/proto/show_episode_state.proto +++ b/protocol/proto/show_episode_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -16,10 +16,3 @@ message EpisodeOfflineState { optional string offline_state = 1; optional uint32 sync_progress = 2; } - -message EpisodePlayState { - optional uint32 time_left = 1; - optional bool is_playable = 2; - optional bool is_played = 3; - optional uint64 last_played_at = 4; -} diff --git a/protocol/proto/show_request.proto b/protocol/proto/show_request.proto index 0f40a1bd..3624fa04 100644 --- a/protocol/proto/show_request.proto +++ b/protocol/proto/show_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; @@ -6,6 +6,7 @@ package spotify.show_cosmos.proto; import "metadata/episode_metadata.proto"; import "metadata/show_metadata.proto"; +import "played_state/episode_played_state.proto"; import "show_episode_state.proto"; import "show_show_state.proto"; import "podcast_virality.proto"; @@ -13,7 +14,10 @@ import "transcripts.proto"; import "podcastextensions.proto"; import "clips_cover.proto"; import "show_access.proto"; +import "podcast_ratings.proto"; +import "greenroom_extension.proto"; +option objc_class_prefix = "SPTShowCosmos"; option optimize_for = CODE_SIZE; message Item { @@ -21,9 +25,10 @@ message Item { optional cosmos_util.proto.EpisodeMetadata episode_metadata = 2; optional EpisodeCollectionState episode_collection_state = 3; optional EpisodeOfflineState episode_offline_state = 4; - optional EpisodePlayState episode_play_state = 5; + optional cosmos_util.proto.EpisodePlayState episode_play_state = 5; optional corex.transcripts.metadata.EpisodeTranscript episode_transcripts = 7; optional podcastvirality.v1.PodcastVirality episode_virality = 8; + optional clips.ClipsCover episode_clips = 9; reserved 6; } @@ -43,6 +48,7 @@ message Response { optional uint32 unranged_length = 7; optional AuxiliarySections auxiliary_sections = 8; optional podcast_paywalls.ShowAccess access_info = 9; + optional uint32 range_offset = 10; reserved 3, "online_data"; } @@ -53,6 +59,9 @@ message AuxiliarySections { optional TrailerSection trailer_section = 3; optional podcast.extensions.PodcastHtmlDescription html_description_section = 5; optional clips.ClipsCover clips_section = 6; + optional ratings.PodcastRating rating_section = 7; + optional greenroom.api.extendedmetadata.v1.GreenroomSection greenroom_section = 8; + optional LatestUnplayedEpisodeSection latest_unplayed_episode_section = 9; reserved 4; } @@ -64,3 +73,7 @@ message ContinueListeningSection { message TrailerSection { optional Item item = 1; } + +message LatestUnplayedEpisodeSection { + optional Item item = 1; +} diff --git a/protocol/proto/show_show_state.proto b/protocol/proto/show_show_state.proto index ab0d1fe3..c9c3548a 100644 --- a/protocol/proto/show_show_state.proto +++ b/protocol/proto/show_show_state.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.show_cosmos.proto; +option objc_class_prefix = "SPTShowCosmos"; option optimize_for = CODE_SIZE; message ShowCollectionState { diff --git a/protocol/proto/social_connect_v2.proto b/protocol/proto/social_connect_v2.proto index 265fbee6..f4d084c8 100644 --- a/protocol/proto/social_connect_v2.proto +++ b/protocol/proto/social_connect_v2.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -15,6 +15,16 @@ message Session { repeated SessionMember session_members = 6; string join_session_uri = 7; bool is_session_owner = 9; + bool is_listening = 10; + bool is_controlling = 11; + bool is_discoverable = 12; + SessionType initial_session_type = 13; + + oneof _host_active_device_id { + string host_active_device_id = 14; + } + + reserved 8; } message SessionMember { @@ -24,6 +34,8 @@ message SessionMember { string display_name = 4; string image_url = 5; string large_image_url = 6; + bool is_listening = 7; + bool is_controlling = 8; } message SessionUpdate { @@ -37,6 +49,13 @@ message DevicesExposure { map devices_exposure = 2; } +enum SessionType { + UNKNOWN_SESSION_TYPE = 0; + IN_PERSON = 3; + REMOTE = 4; + REMOTE_V2 = 5; +} + enum SessionUpdateReason { UNKNOWN_UPDATE_TYPE = 0; NEW_SESSION = 1; @@ -46,6 +65,9 @@ enum SessionUpdateReason { YOU_LEFT = 5; YOU_WERE_KICKED = 6; YOU_JOINED = 7; + PARTICIPANT_PROMOTED_TO_HOST = 8; + DISCOVERABILITY_CHANGED = 9; + USER_KICKED = 10; } enum DeviceExposureStatus { diff --git a/protocol/proto/social_service.proto b/protocol/proto/social_service.proto new file mode 100644 index 00000000..d5c108a8 --- /dev/null +++ b/protocol/proto/social_service.proto @@ -0,0 +1,52 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.social_esperanto.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.social.esperanto.proto"; + +service SocialService { + rpc SetAccessToken(SetAccessTokenRequest) returns (SetAccessTokenResponse); + rpc SubscribeToEvents(SubscribeToEventsRequest) returns (stream SubscribeToEventsResponse); + rpc SubscribeToState(SubscribeToStateRequest) returns (stream SubscribeToStateResponse); +} + +message SetAccessTokenRequest { + string accessToken = 1; +} + +message SetAccessTokenResponse { + +} + +message SubscribeToEventsRequest { + +} + +message SubscribeToEventsResponse { + Error status = 1; + enum Error { + NONE = 0; + FAILED_TO_CONNECT = 1; + USER_DATA_FAIL = 2; + PERMISSIONS = 3; + SERVICE_CONNECT_NOT_PERMITTED = 4; + USER_UNAUTHORIZED = 5; + } + + string description = 2; +} + +message SubscribeToStateRequest { + +} + +message SubscribeToStateResponse { + bool available = 1; + bool enabled = 2; + repeated string missingPermissions = 3; + string accessToken = 4; +} diff --git a/protocol/proto/socialgraph_response_status.proto b/protocol/proto/socialgraph_response_status.proto new file mode 100644 index 00000000..1518daf1 --- /dev/null +++ b/protocol/proto/socialgraph_response_status.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.socialgraph_esperanto.proto; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.socialgraph.esperanto.proto"; + +message ResponseStatus { + int32 status_code = 1; + string reason = 2; +} diff --git a/protocol/proto/socialgraphv2.proto b/protocol/proto/socialgraphv2.proto new file mode 100644 index 00000000..ace70589 --- /dev/null +++ b/protocol/proto/socialgraphv2.proto @@ -0,0 +1,45 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.socialgraph.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.socialgraph.proto"; + +message SocialGraphEntity { + optional string user_uri = 1; + optional string artist_uri = 2; + optional int32 followers_count = 3; + optional int32 following_count = 4; + optional int32 status = 5; + optional bool is_following = 6; + optional bool is_followed = 7; + optional bool is_dismissed = 8; + optional bool is_blocked = 9; + optional int64 following_at = 10; + optional int64 followed_at = 11; + optional int64 dismissed_at = 12; + optional int64 blocked_at = 13; +} + +message SocialGraphRequest { + repeated string target_uris = 1; + optional string source_uri = 2; +} + +message SocialGraphReply { + repeated SocialGraphEntity entities = 1; + optional int32 num_total_entities = 2; +} + +message ChangeNotification { + optional EventType event_type = 1; + repeated SocialGraphEntity entities = 2; +} + +enum EventType { + FOLLOW = 1; + UNFOLLOW = 2; +} diff --git a/protocol/proto/state_restore/ads_rules_inject_tracks.proto b/protocol/proto/state_restore/ads_rules_inject_tracks.proto new file mode 100644 index 00000000..569c8cdf --- /dev/null +++ b/protocol/proto/state_restore/ads_rules_inject_tracks.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "state_restore/provided_track.proto"; + +option optimize_for = CODE_SIZE; + +message AdsRulesInjectTracks { + repeated ProvidedTrack ads = 1; + optional bool is_playing_slot = 2; +} diff --git a/protocol/proto/state_restore/behavior_metadata_rules.proto b/protocol/proto/state_restore/behavior_metadata_rules.proto new file mode 100644 index 00000000..4bb65cd4 --- /dev/null +++ b/protocol/proto/state_restore/behavior_metadata_rules.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message BehaviorMetadataRules { + repeated string page_instance_ids = 1; + repeated string interaction_ids = 2; +} diff --git a/protocol/proto/state_restore/circuit_breaker_rules.proto b/protocol/proto/state_restore/circuit_breaker_rules.proto new file mode 100644 index 00000000..e81eaf57 --- /dev/null +++ b/protocol/proto/state_restore/circuit_breaker_rules.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message CircuitBreakerRules { + repeated string discarded_track_uids = 1; + required int32 num_errored_tracks = 2; + required bool context_track_played = 3; +} diff --git a/protocol/proto/state_restore/context_player_rules.proto b/protocol/proto/state_restore/context_player_rules.proto new file mode 100644 index 00000000..b06bf8e8 --- /dev/null +++ b/protocol/proto/state_restore/context_player_rules.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "state_restore/context_player_rules_base.proto"; +import "state_restore/mft_rules.proto"; + +option optimize_for = CODE_SIZE; + +message ContextPlayerRules { + optional ContextPlayerRulesBase base = 1; + optional MftRules mft_rules = 2; + map sub_rules = 3; +} diff --git a/protocol/proto/state_restore/context_player_rules_base.proto b/protocol/proto/state_restore/context_player_rules_base.proto new file mode 100644 index 00000000..da973bba --- /dev/null +++ b/protocol/proto/state_restore/context_player_rules_base.proto @@ -0,0 +1,33 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "state_restore/ads_rules_inject_tracks.proto"; +import "state_restore/behavior_metadata_rules.proto"; +import "state_restore/circuit_breaker_rules.proto"; +import "state_restore/explicit_content_rules.proto"; +import "state_restore/explicit_request_rules.proto"; +import "state_restore/mft_rules_core.proto"; +import "state_restore/mod_rules_interruptions.proto"; +import "state_restore/music_injection_rules.proto"; +import "state_restore/remove_banned_tracks_rules.proto"; +import "state_restore/resume_points_rules.proto"; +import "state_restore/track_error_rules.proto"; + +option optimize_for = CODE_SIZE; + +message ContextPlayerRulesBase { + optional BehaviorMetadataRules behavior_metadata_rules = 1; + optional CircuitBreakerRules circuit_breaker_rules = 2; + optional ExplicitContentRules explicit_content_rules = 3; + optional ExplicitRequestRules explicit_request_rules = 4; + optional MusicInjectionRules music_injection_rules = 5; + optional RemoveBannedTracksRules remove_banned_tracks_rules = 6; + optional ResumePointsRules resume_points_rules = 7; + optional TrackErrorRules track_error_rules = 8; + optional AdsRulesInjectTracks ads_rules_inject_tracks = 9; + optional MftRulesCore mft_rules_core = 10; + optional ModRulesInterruptions mod_rules_interruptions = 11; +} diff --git a/protocol/proto/state_restore/explicit_content_rules.proto b/protocol/proto/state_restore/explicit_content_rules.proto new file mode 100644 index 00000000..271ad6ea --- /dev/null +++ b/protocol/proto/state_restore/explicit_content_rules.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message ExplicitContentRules { + required bool filter_explicit_content = 1; + required bool filter_age_restricted_content = 2; +} diff --git a/protocol/proto/state_restore/explicit_request_rules.proto b/protocol/proto/state_restore/explicit_request_rules.proto new file mode 100644 index 00000000..babda5cb --- /dev/null +++ b/protocol/proto/state_restore/explicit_request_rules.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message ExplicitRequestRules { + required bool always_play_something = 1; +} diff --git a/protocol/proto/state_restore/mft_context_history.proto b/protocol/proto/state_restore/mft_context_history.proto new file mode 100644 index 00000000..48e77205 --- /dev/null +++ b/protocol/proto/state_restore/mft_context_history.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "context_track.proto"; + +option optimize_for = CODE_SIZE; + +message MftContextHistoryEntry { + required ContextTrack track = 1; + required int64 timestamp = 2; + optional int64 position = 3; +} + +message MftContextHistory { + map lookup = 1; +} diff --git a/protocol/proto/state_restore/mft_context_switch_rules.proto b/protocol/proto/state_restore/mft_context_switch_rules.proto new file mode 100644 index 00000000..d01e9298 --- /dev/null +++ b/protocol/proto/state_restore/mft_context_switch_rules.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message MftContextSwitchRules { + required bool has_played_track = 1; + required bool enabled = 2; +} diff --git a/protocol/proto/state_restore/mft_fallback_page_history.proto b/protocol/proto/state_restore/mft_fallback_page_history.proto new file mode 100644 index 00000000..54d15e8d --- /dev/null +++ b/protocol/proto/state_restore/mft_fallback_page_history.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message ContextAndPage { + required string context_uri = 1; + required string fallback_page_url = 2; +} + +message MftFallbackPageHistory { + repeated ContextAndPage context_to_fallback_page = 1; +} diff --git a/protocol/proto/state_restore/mft_rules.proto b/protocol/proto/state_restore/mft_rules.proto new file mode 100644 index 00000000..141cdac7 --- /dev/null +++ b/protocol/proto/state_restore/mft_rules.proto @@ -0,0 +1,38 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "state_restore/context_player_rules_base.proto"; + +option optimize_for = CODE_SIZE; + +message PlayEvents { + required int32 max_consecutive = 1; + required int32 max_occurrences_in_period = 2; + required int64 period = 3; +} + +message SkipEvents { + required int32 max_occurrences_in_period = 1; + required int64 period = 2; +} + +message Context { + required int32 min_tracks = 1; +} + +message MftConfiguration { + optional PlayEvents track = 1; + optional PlayEvents album = 2; + optional PlayEvents artist = 3; + optional SkipEvents skip = 4; + optional Context context = 5; +} + +message MftRules { + required bool locked = 1; + optional MftConfiguration config = 2; + map forward_rules = 3; +} diff --git a/protocol/proto/state_restore/mft_rules_core.proto b/protocol/proto/state_restore/mft_rules_core.proto new file mode 100644 index 00000000..05549624 --- /dev/null +++ b/protocol/proto/state_restore/mft_rules_core.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "state_restore/mft_context_switch_rules.proto"; +import "state_restore/mft_rules_inject_filler_tracks.proto"; + +option optimize_for = CODE_SIZE; + +message MftRulesCore { + required MftRulesInjectFillerTracks inject_filler_tracks = 1; + required MftContextSwitchRules context_switch_rules = 2; + repeated string feature_classes = 3; +} diff --git a/protocol/proto/state_restore/mft_rules_inject_filler_tracks.proto b/protocol/proto/state_restore/mft_rules_inject_filler_tracks.proto new file mode 100644 index 00000000..b5b8c657 --- /dev/null +++ b/protocol/proto/state_restore/mft_rules_inject_filler_tracks.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "context_track.proto"; +import "state_restore/random_source.proto"; + +option optimize_for = CODE_SIZE; + +message MftRandomTrackInjection { + required RandomSource random_source = 1; + required int32 offset = 2; +} + +message MftRulesInjectFillerTracks { + repeated ContextTrack fallback_tracks = 1; + required MftRandomTrackInjection padding_track_injection = 2; + required RandomSource random_source = 3; + required bool filter_explicit_content = 4; + repeated string feature_classes = 5; +} diff --git a/protocol/proto/state_restore/mft_state.proto b/protocol/proto/state_restore/mft_state.proto new file mode 100644 index 00000000..8f5f9561 --- /dev/null +++ b/protocol/proto/state_restore/mft_state.proto @@ -0,0 +1,31 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message EventList { + repeated int64 event_times = 1; +} + +message LastEvent { + required string uri = 1; + required int32 when = 2; +} + +message History { + map when = 1; + required LastEvent last = 2; +} + +message MftState { + required History track = 1; + required History social_track = 2; + required History album = 3; + required History artist = 4; + required EventList skip = 5; + required int32 time = 6; + required bool did_skip = 7; +} diff --git a/protocol/proto/state_restore/mod_interruption_state.proto b/protocol/proto/state_restore/mod_interruption_state.proto new file mode 100644 index 00000000..e09ffe13 --- /dev/null +++ b/protocol/proto/state_restore/mod_interruption_state.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "context_track.proto"; +import "state_restore/provided_track.proto"; + +option optimize_for = CODE_SIZE; + +message StoredInterruption { + required ContextTrack context_track = 1; + required int64 fetched_at = 2; +} + +message ModInterruptionState { + optional string context_uri = 1; + optional ProvidedTrack last_track = 2; + map active_play_count = 3; + repeated StoredInterruption active_play_interruptions = 4; + repeated StoredInterruption repeat_play_interruptions = 5; +} diff --git a/protocol/proto/state_restore/mod_rules_interruptions.proto b/protocol/proto/state_restore/mod_rules_interruptions.proto new file mode 100644 index 00000000..1b965ccd --- /dev/null +++ b/protocol/proto/state_restore/mod_rules_interruptions.proto @@ -0,0 +1,27 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "player_license.proto"; +import "state_restore/provided_track.proto"; + +option optimize_for = CODE_SIZE; + +message ModRulesInterruptions { + optional ProvidedTrack seek_repeat_track = 1; + required uint32 prng_seed = 2; + required bool support_video = 3; + required bool is_active_action = 4; + required bool is_in_seek_repeat = 5; + required bool has_tp_api_restrictions = 6; + required InterruptionSource interruption_source = 7; + required PlayerLicense license = 8; +} + +enum InterruptionSource { + Context_IS = 1; + SAS = 2; + NoInterruptions = 3; +} diff --git a/protocol/proto/state_restore/music_injection_rules.proto b/protocol/proto/state_restore/music_injection_rules.proto new file mode 100644 index 00000000..5ae18bce --- /dev/null +++ b/protocol/proto/state_restore/music_injection_rules.proto @@ -0,0 +1,25 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message InjectionSegment { + required string track_uri = 1; + optional int64 start = 2; + optional int64 stop = 3; + required int64 duration = 4; +} + +message InjectionModel { + optional string episode_uri = 1; + optional int64 total_duration = 2; + repeated InjectionSegment segments = 3; +} + +message MusicInjectionRules { + optional InjectionModel injection_model = 1; + optional bytes playback_id = 2; +} diff --git a/protocol/proto/state_restore/player_session_queue.proto b/protocol/proto/state_restore/player_session_queue.proto new file mode 100644 index 00000000..22ee7941 --- /dev/null +++ b/protocol/proto/state_restore/player_session_queue.proto @@ -0,0 +1,27 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message SessionJson { + optional string json = 1; +} + +message QueuedSession { + optional Trigger trigger = 1; + optional SessionJson session = 2; +} + +message PlayerSessionQueue { + optional SessionJson active = 1; + repeated SessionJson pushed = 2; + repeated QueuedSession queued = 3; +} + +enum Trigger { + DID_GO_PAST_TRACK = 1; + DID_GO_PAST_CONTEXT = 2; +} diff --git a/protocol/proto/state_restore/provided_track.proto b/protocol/proto/state_restore/provided_track.proto new file mode 100644 index 00000000..a61010e5 --- /dev/null +++ b/protocol/proto/state_restore/provided_track.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "restrictions.proto"; + +option optimize_for = CODE_SIZE; + +message ProvidedTrack { + optional string uid = 1; + optional string uri = 2; + map metadata = 3; + optional string provider = 4; + repeated string removed = 5; + repeated string blocked = 6; + map internal_metadata = 7; + optional Restrictions restrictions = 8; +} diff --git a/protocol/proto/state_restore/random_source.proto b/protocol/proto/state_restore/random_source.proto new file mode 100644 index 00000000..f1ad1019 --- /dev/null +++ b/protocol/proto/state_restore/random_source.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message RandomSource { + required uint64 random_0 = 1; + required uint64 random_1 = 2; +} diff --git a/protocol/proto/state_restore/remove_banned_tracks_rules.proto b/protocol/proto/state_restore/remove_banned_tracks_rules.proto new file mode 100644 index 00000000..9db5c70c --- /dev/null +++ b/protocol/proto/state_restore/remove_banned_tracks_rules.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message Strings { + repeated string strings = 1; +} + +message RemoveBannedTracksRules { + repeated string banned_tracks = 1; + repeated string banned_albums = 2; + repeated string banned_artists = 3; + map banned_context_tracks = 4; +} diff --git a/protocol/proto/state_restore/resume_points_rules.proto b/protocol/proto/state_restore/resume_points_rules.proto new file mode 100644 index 00000000..6f2618a9 --- /dev/null +++ b/protocol/proto/state_restore/resume_points_rules.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message ResumePoint { + required bool is_fully_played = 1; + required int64 position = 2; + required int64 timestamp = 3; +} + +message ResumePointsRules { + map resume_points = 1; +} diff --git a/protocol/proto/state_restore/track_error_rules.proto b/protocol/proto/state_restore/track_error_rules.proto new file mode 100644 index 00000000..e13b8562 --- /dev/null +++ b/protocol/proto/state_restore/track_error_rules.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +option optimize_for = CODE_SIZE; + +message TrackErrorRules { + repeated string reasons = 1; + required int32 num_attempted_tracks = 2; + required int32 num_failed_tracks = 3; +} diff --git a/protocol/proto/status.proto b/protocol/proto/status.proto new file mode 100644 index 00000000..1293af57 --- /dev/null +++ b/protocol/proto/status.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.collection_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message Status { + int32 code = 1; + string reason = 2; +} diff --git a/protocol/proto/status_code.proto b/protocol/proto/status_code.proto index 8e813d25..abc8bd49 100644 --- a/protocol/proto/status_code.proto +++ b/protocol/proto/status_code.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -9,4 +9,6 @@ option java_package = "com.spotify.stream_reporting_esperanto.proto"; enum StatusCode { SUCCESS = 0; + NO_PLAYBACK_ID = 1; + EVENT_SENDER_ERROR = 2; } diff --git a/protocol/proto/stream_end_request.proto b/protocol/proto/stream_end_request.proto index 5ef8be7f..ed72fd51 100644 --- a/protocol/proto/stream_end_request.proto +++ b/protocol/proto/stream_end_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -6,13 +6,14 @@ package spotify.stream_reporting_esperanto.proto; import "stream_handle.proto"; import "play_reason.proto"; -import "play_source.proto"; +import "media_format.proto"; option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; message StreamEndRequest { StreamHandle stream_handle = 1; - PlaySource source_end = 2; + string source_end = 2; PlayReason reason_end = 3; + MediaFormat format = 4; } diff --git a/protocol/proto/stream_prepare_request.proto b/protocol/proto/stream_prepare_request.proto deleted file mode 100644 index ce22e8eb..00000000 --- a/protocol/proto/stream_prepare_request.proto +++ /dev/null @@ -1,39 +0,0 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) - -syntax = "proto3"; - -package spotify.stream_reporting_esperanto.proto; - -import "play_reason.proto"; -import "play_source.proto"; -import "streaming_rule.proto"; - -option objc_class_prefix = "ESP"; -option java_package = "com.spotify.stream_reporting_esperanto.proto"; - -message StreamPrepareRequest { - string playback_id = 1; - string parent_playback_id = 2; - string parent_play_track = 3; - string video_session_id = 4; - string play_context = 5; - string uri = 6; - string displayed_uri = 7; - string feature_identifier = 8; - string feature_version = 9; - string view_uri = 10; - string provider = 11; - string referrer = 12; - string referrer_version = 13; - string referrer_vendor = 14; - StreamingRule streaming_rule = 15; - string connect_controller_device_id = 16; - string page_instance_id = 17; - string interaction_id = 18; - PlaySource source_start = 19; - PlayReason reason_start = 20; - bool is_live = 22; - bool is_shuffle = 23; - bool is_offlined = 24; - bool is_incognito = 25; -} diff --git a/protocol/proto/stream_seek_request.proto b/protocol/proto/stream_seek_request.proto index 3736abf9..7d99169e 100644 --- a/protocol/proto/stream_seek_request.proto +++ b/protocol/proto/stream_seek_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -11,4 +11,6 @@ option java_package = "com.spotify.stream_reporting_esperanto.proto"; message StreamSeekRequest { StreamHandle stream_handle = 1; + uint64 from_position = 3; + uint64 to_position = 4; } diff --git a/protocol/proto/stream_start_request.proto b/protocol/proto/stream_start_request.proto index 3c4bfbb6..656016a6 100644 --- a/protocol/proto/stream_start_request.proto +++ b/protocol/proto/stream_start_request.proto @@ -1,20 +1,44 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.stream_reporting_esperanto.proto; -import "format.proto"; import "media_type.proto"; -import "stream_handle.proto"; +import "play_reason.proto"; +import "streaming_rule.proto"; option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; message StreamStartRequest { - StreamHandle stream_handle = 1; - string media_id = 2; - MediaType media_type = 3; - Format format = 4; - uint64 playback_start_time = 5; + string playback_id = 1; + string parent_playback_id = 2; + string parent_play_track = 3; + string video_session_id = 4; + string play_context = 5; + string uri = 6; + string displayed_uri = 7; + string feature_identifier = 8; + string feature_version = 9; + string view_uri = 10; + string provider = 11; + string referrer = 12; + string referrer_version = 13; + string referrer_vendor = 14; + StreamingRule streaming_rule = 15; + string connect_controller_device_id = 16; + string page_instance_id = 17; + string interaction_id = 18; + string source_start = 19; + PlayReason reason_start = 20; + bool is_shuffle = 23; + bool is_incognito = 25; + string media_id = 28; + MediaType media_type = 29; + uint64 playback_start_time = 30; + uint64 start_position = 31; + bool is_live = 32; + bool stream_was_offlined = 33; + bool client_offline = 34; } diff --git a/protocol/proto/stream_prepare_response.proto b/protocol/proto/stream_start_response.proto similarity index 57% rename from protocol/proto/stream_prepare_response.proto rename to protocol/proto/stream_start_response.proto index 2f5a2c4e..98af2976 100644 --- a/protocol/proto/stream_prepare_response.proto +++ b/protocol/proto/stream_start_response.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -10,9 +10,7 @@ import "stream_handle.proto"; option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; -message StreamPrepareResponse { - oneof response { - StatusResponse status = 1; - StreamHandle stream_handle = 2; - } +message StreamStartResponse { + StatusResponse status = 1; + StreamHandle stream_handle = 2; } diff --git a/protocol/proto/streaming_rule.proto b/protocol/proto/streaming_rule.proto index d72d7ca5..9593fdef 100644 --- a/protocol/proto/streaming_rule.proto +++ b/protocol/proto/streaming_rule.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -8,10 +8,9 @@ option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; enum StreamingRule { - RULE_UNSET = 0; - RULE_NONE = 1; - RULE_DMCA_RADIO = 2; - RULE_PREVIEW = 3; - RULE_WIFI = 4; - RULE_SHUFFLE_MODE = 5; + STREAMING_RULE_NONE = 0; + STREAMING_RULE_DMCA_RADIO = 1; + STREAMING_RULE_PREVIEW = 2; + STREAMING_RULE_WIFI = 3; + STREAMING_RULE_SHUFFLE_MODE = 4; } diff --git a/protocol/proto/sync_request.proto b/protocol/proto/sync_request.proto index 090f8dce..b2d77625 100644 --- a/protocol/proto/sync_request.proto +++ b/protocol/proto/sync_request.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.playlist.cosmos.proto; +option objc_class_prefix = "SPTPlaylist"; option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "com.spotify.playlist.proto"; diff --git a/protocol/proto/test_request_failure.proto b/protocol/proto/test_request_failure.proto deleted file mode 100644 index 036e38e1..00000000 --- a/protocol/proto/test_request_failure.proto +++ /dev/null @@ -1,14 +0,0 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - -syntax = "proto2"; - -package spotify.image.proto; - -option optimize_for = CODE_SIZE; - -message TestRequestFailure { - optional string request = 1; - optional string source = 2; - optional string error = 3; - optional int64 result = 4; -} diff --git a/protocol/proto/track_offlining_cosmos_response.proto b/protocol/proto/track_offlining_cosmos_response.proto deleted file mode 100644 index bb650607..00000000 --- a/protocol/proto/track_offlining_cosmos_response.proto +++ /dev/null @@ -1,24 +0,0 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - -syntax = "proto2"; - -package spotify.track_offlining_cosmos.proto; - -option optimize_for = CODE_SIZE; - -message DecoratedTrack { - optional string uri = 1; - optional string title = 2; -} - -message ListResponse { - repeated string uri = 1; -} - -message DecorateResponse { - repeated DecoratedTrack tracks = 1; -} - -message StatusResponse { - optional bool offline = 1; -} diff --git a/protocol/proto/tts-resolve.proto b/protocol/proto/tts-resolve.proto index 89956843..adb50854 100644 --- a/protocol/proto/tts-resolve.proto +++ b/protocol/proto/tts-resolve.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -36,8 +36,12 @@ message ResolveRequest { UNSET_TTS_PROVIDER = 0; CLOUD_TTS = 1; READSPEAKER = 2; + POLLY = 3; + WELL_SAID = 4; } + int32 sample_rate_hz = 7; + oneof prompt { string text = 1; string ssml = 2; diff --git a/protocol/proto/unfinished_episodes_request.proto b/protocol/proto/unfinished_episodes_request.proto index 1e152bd6..68e5f903 100644 --- a/protocol/proto/unfinished_episodes_request.proto +++ b/protocol/proto/unfinished_episodes_request.proto @@ -1,19 +1,21 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto2"; package spotify.show_cosmos.unfinished_episodes_request.proto; import "metadata/episode_metadata.proto"; +import "played_state/episode_played_state.proto"; import "show_episode_state.proto"; +option objc_class_prefix = "SPTShowCosmosUnfinshedEpisodes"; option optimize_for = CODE_SIZE; message Episode { optional cosmos_util.proto.EpisodeMetadata episode_metadata = 1; optional show_cosmos.proto.EpisodeCollectionState episode_collection_state = 2; optional show_cosmos.proto.EpisodeOfflineState episode_offline_state = 3; - optional show_cosmos.proto.EpisodePlayState episode_play_state = 4; + optional cosmos_util.proto.EpisodePlayState episode_play_state = 4; optional string link = 5; } diff --git a/protocol/proto/your_library_contains_request.proto b/protocol/proto/your_library_contains_request.proto index 33672bad..bbb43c20 100644 --- a/protocol/proto/your_library_contains_request.proto +++ b/protocol/proto/your_library_contains_request.proto @@ -1,11 +1,14 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.your_library.proto; +import "your_library_pseudo_playlist_config.proto"; + option optimize_for = CODE_SIZE; message YourLibraryContainsRequest { repeated string requested_uri = 3; + YourLibraryPseudoPlaylistConfig pseudo_playlist_config = 4; } diff --git a/protocol/proto/your_library_decorate_request.proto b/protocol/proto/your_library_decorate_request.proto index e3fccc29..6b77a976 100644 --- a/protocol/proto/your_library_decorate_request.proto +++ b/protocol/proto/your_library_decorate_request.proto @@ -1,17 +1,14 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.your_library.proto; -import "your_library_request.proto"; +import "your_library_pseudo_playlist_config.proto"; option optimize_for = CODE_SIZE; message YourLibraryDecorateRequest { repeated string requested_uri = 3; - YourLibraryLabelAndImage liked_songs_label_and_image = 201; - YourLibraryLabelAndImage your_episodes_label_and_image = 202; - YourLibraryLabelAndImage new_episodes_label_and_image = 203; - YourLibraryLabelAndImage local_files_label_and_image = 204; + YourLibraryPseudoPlaylistConfig pseudo_playlist_config = 6; } diff --git a/protocol/proto/your_library_decorate_response.proto b/protocol/proto/your_library_decorate_response.proto index dab14203..125d5c33 100644 --- a/protocol/proto/your_library_decorate_response.proto +++ b/protocol/proto/your_library_decorate_response.proto @@ -1,10 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.your_library.proto; -import "your_library_response.proto"; +import "your_library_decorated_entity.proto"; option optimize_for = CODE_SIZE; @@ -14,6 +14,6 @@ message YourLibraryDecorateResponseHeader { message YourLibraryDecorateResponse { YourLibraryDecorateResponseHeader header = 1; - repeated YourLibraryResponseEntity entity = 2; + repeated YourLibraryDecoratedEntity entity = 2; string error = 99; } diff --git a/protocol/proto/your_library_decorated_entity.proto b/protocol/proto/your_library_decorated_entity.proto new file mode 100644 index 00000000..c31b45eb --- /dev/null +++ b/protocol/proto/your_library_decorated_entity.proto @@ -0,0 +1,105 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.your_library.proto; + +option optimize_for = CODE_SIZE; + +message YourLibraryEntityInfo { + string key = 1; + string name = 2; + string uri = 3; + string group_label = 5; + string image_uri = 6; + bool pinned = 7; + + Pinnable pinnable = 8; + enum Pinnable { + YES = 0; + NO_IN_FOLDER = 1; + } + + Offline.Availability offline_availability = 9; +} + +message Offline { + enum Availability { + UNKNOWN = 0; + NO = 1; + YES = 2; + DOWNLOADING = 3; + WAITING = 4; + } +} + +message YourLibraryAlbumExtraInfo { + string artist_name = 1; +} + +message YourLibraryArtistExtraInfo { + +} + +message YourLibraryPlaylistExtraInfo { + string creator_name = 1; + bool is_loading = 5; + bool can_view = 6; +} + +message YourLibraryShowExtraInfo { + string creator_name = 1; + int64 publish_date = 4; + bool is_music_and_talk = 5; + int32 number_of_downloaded_episodes = 6; +} + +message YourLibraryFolderExtraInfo { + int32 number_of_playlists = 2; + int32 number_of_folders = 3; +} + +message YourLibraryLikedSongsExtraInfo { + int32 number_of_songs = 3; +} + +message YourLibraryYourEpisodesExtraInfo { + int32 number_of_downloaded_episodes = 4; +} + +message YourLibraryNewEpisodesExtraInfo { + int64 publish_date = 1; +} + +message YourLibraryLocalFilesExtraInfo { + int32 number_of_files = 1; +} + +message YourLibraryBookExtraInfo { + string author_name = 1; +} + +message YourLibraryDecoratedEntity { + YourLibraryEntityInfo entity_info = 1; + + oneof entity { + YourLibraryAlbumExtraInfo album = 2; + YourLibraryArtistExtraInfo artist = 3; + YourLibraryPlaylistExtraInfo playlist = 4; + YourLibraryShowExtraInfo show = 5; + YourLibraryFolderExtraInfo folder = 6; + YourLibraryLikedSongsExtraInfo liked_songs = 8; + YourLibraryYourEpisodesExtraInfo your_episodes = 9; + YourLibraryNewEpisodesExtraInfo new_episodes = 10; + YourLibraryLocalFilesExtraInfo local_files = 11; + YourLibraryBookExtraInfo book = 12; + } +} + +message YourLibraryAvailableEntityTypes { + bool albums = 1; + bool artists = 2; + bool playlists = 3; + bool shows = 4; + bool books = 5; +} diff --git a/protocol/proto/your_library_entity.proto b/protocol/proto/your_library_entity.proto index acb5afe7..897fc6c1 100644 --- a/protocol/proto/your_library_entity.proto +++ b/protocol/proto/your_library_entity.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -9,13 +9,24 @@ import "collection_index.proto"; option optimize_for = CODE_SIZE; +message YourLibraryShowWrapper { + collection.proto.CollectionAlbumLikeEntry show = 1; + string uri = 2; +} + +message YourLibraryBookWrapper { + collection.proto.CollectionAlbumLikeEntry book = 1; + string uri = 2; +} + message YourLibraryEntity { bool pinned = 1; oneof entity { - collection.proto.CollectionAlbumEntry album = 2; - YourLibraryArtistEntity artist = 3; + collection.proto.CollectionAlbumLikeEntry album = 2; + collection.proto.CollectionArtistEntry artist = 3; YourLibraryRootlistEntity rootlist_entity = 4; - YourLibraryShowEntity show = 5; + YourLibraryShowWrapper show = 7; + YourLibraryBookWrapper book = 8; } } diff --git a/protocol/proto/your_library_index.proto b/protocol/proto/your_library_index.proto index 2d452dd5..835c0fa2 100644 --- a/protocol/proto/your_library_index.proto +++ b/protocol/proto/your_library_index.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; @@ -6,18 +6,11 @@ package spotify.your_library.proto; option optimize_for = CODE_SIZE; -message YourLibraryArtistEntity { - string uri = 1; - string name = 2; - string image_uri = 3; - int64 add_time = 4; -} - message YourLibraryRootlistPlaylist { string image_uri = 1; - bool is_on_demand_in_free = 2; bool is_loading = 3; int32 rootlist_index = 4; + bool can_view = 5; } message YourLibraryRootlistFolder { @@ -48,13 +41,3 @@ message YourLibraryRootlistEntity { YourLibraryRootlistCollection collection = 7; } } - -message YourLibraryShowEntity { - string uri = 1; - string name = 2; - string creator_name = 3; - string image_uri = 4; - int64 add_time = 5; - bool is_music_and_talk = 6; - int64 publish_date = 7; -} diff --git a/protocol/proto/your_library_pseudo_playlist_config.proto b/protocol/proto/your_library_pseudo_playlist_config.proto new file mode 100644 index 00000000..77c9bb53 --- /dev/null +++ b/protocol/proto/your_library_pseudo_playlist_config.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.73.517 (macOS) + +syntax = "proto3"; + +package spotify.your_library.proto; + +option optimize_for = CODE_SIZE; + +message YourLibraryLabelAndImage { + string label = 1; + string image = 2; +} + +message YourLibraryPseudoPlaylistConfig { + YourLibraryLabelAndImage liked_songs = 1; + YourLibraryLabelAndImage your_episodes = 2; + YourLibraryLabelAndImage new_episodes = 3; + YourLibraryLabelAndImage local_files = 4; +} diff --git a/protocol/proto/your_library_request.proto b/protocol/proto/your_library_request.proto index a75a0544..917a1add 100644 --- a/protocol/proto/your_library_request.proto +++ b/protocol/proto/your_library_request.proto @@ -1,74 +1,18 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.your_library.proto; +import "your_library_pseudo_playlist_config.proto"; + option optimize_for = CODE_SIZE; -message YourLibraryRequestEntityInfo { - -} - -message YourLibraryRequestAlbumExtraInfo { - -} - -message YourLibraryRequestArtistExtraInfo { - -} - -message YourLibraryRequestPlaylistExtraInfo { - -} - -message YourLibraryRequestShowExtraInfo { - -} - -message YourLibraryRequestFolderExtraInfo { - -} - -message YourLibraryLabelAndImage { - string label = 1; - string image = 2; -} - -message YourLibraryRequestLikedSongsExtraInfo { - YourLibraryLabelAndImage label_and_image = 101; -} - -message YourLibraryRequestYourEpisodesExtraInfo { - YourLibraryLabelAndImage label_and_image = 101; -} - -message YourLibraryRequestNewEpisodesExtraInfo { - YourLibraryLabelAndImage label_and_image = 101; -} - -message YourLibraryRequestLocalFilesExtraInfo { - YourLibraryLabelAndImage label_and_image = 101; -} - -message YourLibraryRequestEntity { - YourLibraryRequestEntityInfo entityInfo = 1; - YourLibraryRequestAlbumExtraInfo album = 2; - YourLibraryRequestArtistExtraInfo artist = 3; - YourLibraryRequestPlaylistExtraInfo playlist = 4; - YourLibraryRequestShowExtraInfo show = 5; - YourLibraryRequestFolderExtraInfo folder = 6; - YourLibraryRequestLikedSongsExtraInfo liked_songs = 8; - YourLibraryRequestYourEpisodesExtraInfo your_episodes = 9; - YourLibraryRequestNewEpisodesExtraInfo new_episodes = 10; - YourLibraryRequestLocalFilesExtraInfo local_files = 11; -} - message YourLibraryRequestHeader { bool remaining_entities = 9; } message YourLibraryRequest { YourLibraryRequestHeader header = 1; - YourLibraryRequestEntity entity = 2; + YourLibraryPseudoPlaylistConfig pseudo_playlist_config = 4; } diff --git a/protocol/proto/your_library_response.proto b/protocol/proto/your_library_response.proto index 124b35b4..c354ff5b 100644 --- a/protocol/proto/your_library_response.proto +++ b/protocol/proto/your_library_response.proto @@ -1,112 +1,23 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.1.73.517 (macOS) syntax = "proto3"; package spotify.your_library.proto; +import "your_library_decorated_entity.proto"; + option optimize_for = CODE_SIZE; -message YourLibraryEntityInfo { - string key = 1; - string name = 2; - string uri = 3; - string group_label = 5; - string image_uri = 6; - bool pinned = 7; - - Pinnable pinnable = 8; - enum Pinnable { - YES = 0; - NO_IN_FOLDER = 1; - } -} - -message Offline { - enum Availability { - UNKNOWN = 0; - NO = 1; - YES = 2; - DOWNLOADING = 3; - WAITING = 4; - } -} - -message YourLibraryAlbumExtraInfo { - string artist_name = 1; - Offline.Availability offline_availability = 3; -} - -message YourLibraryArtistExtraInfo { - int32 num_tracks_in_collection = 1; -} - -message YourLibraryPlaylistExtraInfo { - string creator_name = 1; - Offline.Availability offline_availability = 3; - bool is_loading = 5; -} - -message YourLibraryShowExtraInfo { - string creator_name = 1; - Offline.Availability offline_availability = 3; - int64 publish_date = 4; - bool is_music_and_talk = 5; -} - -message YourLibraryFolderExtraInfo { - int32 number_of_playlists = 2; - int32 number_of_folders = 3; -} - -message YourLibraryLikedSongsExtraInfo { - Offline.Availability offline_availability = 2; - int32 number_of_songs = 3; -} - -message YourLibraryYourEpisodesExtraInfo { - Offline.Availability offline_availability = 2; - int32 number_of_episodes = 3; -} - -message YourLibraryNewEpisodesExtraInfo { - int64 publish_date = 1; -} - -message YourLibraryLocalFilesExtraInfo { - int32 number_of_files = 1; -} - -message YourLibraryResponseEntity { - YourLibraryEntityInfo entityInfo = 1; - - oneof entity { - YourLibraryAlbumExtraInfo album = 2; - YourLibraryArtistExtraInfo artist = 3; - YourLibraryPlaylistExtraInfo playlist = 4; - YourLibraryShowExtraInfo show = 5; - YourLibraryFolderExtraInfo folder = 6; - YourLibraryLikedSongsExtraInfo liked_songs = 8; - YourLibraryYourEpisodesExtraInfo your_episodes = 9; - YourLibraryNewEpisodesExtraInfo new_episodes = 10; - YourLibraryLocalFilesExtraInfo local_files = 11; - } -} - message YourLibraryResponseHeader { - bool has_albums = 1; - bool has_artists = 2; - bool has_playlists = 3; - bool has_shows = 4; - bool has_downloaded_albums = 5; - bool has_downloaded_artists = 6; - bool has_downloaded_playlists = 7; - bool has_downloaded_shows = 8; int32 remaining_entities = 9; bool is_loading = 12; + YourLibraryAvailableEntityTypes has = 13; + YourLibraryAvailableEntityTypes has_downloaded = 14; + string folder_name = 15; } message YourLibraryResponse { YourLibraryResponseHeader header = 1; - repeated YourLibraryResponseEntity entity = 2; + repeated YourLibraryDecoratedEntity entity = 2; string error = 99; } From 9a0d2390b7ed30b482e434883a8f3bf879bdb2b2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 11 Dec 2021 00:03:35 +0100 Subject: [PATCH 65/95] Get user attributes and updates --- Cargo.lock | 11 ++++ connect/src/spirc.rs | 82 +++++++++++++++++++++++++++- core/Cargo.toml | 1 + core/src/mercury/mod.rs | 21 +++++++ core/src/session.rs | 77 +++++++++++++++++++++++++- protocol/build.rs | 1 + protocol/proto/user_attributes.proto | 29 ++++++++++ 7 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 protocol/proto/user_attributes.proto diff --git a/Cargo.lock b/Cargo.lock index d4501fef..5aa66853 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1270,6 +1270,7 @@ dependencies = [ "pbkdf2", "priority-queue", "protobuf", + "quick-xml", "rand", "serde", "serde_json", @@ -1950,6 +1951,16 @@ dependencies = [ "protobuf-codegen", ] +[[package]] +name = "quick-xml" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.10" diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index e64e35a5..bc596bfc 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -6,14 +6,17 @@ use std::time::{SystemTime, UNIX_EPOCH}; use crate::context::StationContext; use crate::core::config::ConnectConfig; use crate::core::mercury::{MercuryError, MercurySender}; -use crate::core::session::Session; +use crate::core::session::{Session, UserAttributes}; use crate::core::spotify_id::SpotifyId; use crate::core::util::SeqGenerator; use crate::core::version; use crate::playback::mixer::Mixer; use crate::playback::player::{Player, PlayerEvent, PlayerEventChannel}; + use crate::protocol; +use crate::protocol::explicit_content_pubsub::UserAttributesUpdate; use crate::protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef}; +use crate::protocol::user_attributes::UserAttributesMutation; use futures_util::future::{self, FusedFuture}; use futures_util::stream::FusedStream; @@ -58,6 +61,8 @@ struct SpircTask { play_status: SpircPlayStatus, subscription: BoxedStream, + user_attributes_update: BoxedStream, + user_attributes_mutation: BoxedStream, sender: MercurySender, commands: Option>, player_events: Option, @@ -248,6 +253,30 @@ impl Spirc { }), ); + let user_attributes_update = Box::pin( + session + .mercury() + .listen_for("spotify:user:attributes:update") + .map(UnboundedReceiverStream::new) + .flatten_stream() + .map(|response| -> UserAttributesUpdate { + let data = response.payload.first().unwrap(); + UserAttributesUpdate::parse_from_bytes(data).unwrap() + }), + ); + + let user_attributes_mutation = Box::pin( + session + .mercury() + .listen_for("spotify:user:attributes:mutated") + .map(UnboundedReceiverStream::new) + .flatten_stream() + .map(|response| -> UserAttributesMutation { + let data = response.payload.first().unwrap(); + UserAttributesMutation::parse_from_bytes(data).unwrap() + }), + ); + let sender = session.mercury().sender(uri); let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); @@ -276,6 +305,8 @@ impl Spirc { play_status: SpircPlayStatus::Stopped, subscription, + user_attributes_update, + user_attributes_mutation, sender, commands: Some(cmd_rx), player_events: Some(player_events), @@ -344,6 +375,20 @@ impl SpircTask { break; } }, + user_attributes_update = self.user_attributes_update.next() => match user_attributes_update { + Some(attributes) => self.handle_user_attributes_update(attributes), + None => { + error!("user attributes update selected, but none received"); + break; + } + }, + user_attributes_mutation = self.user_attributes_mutation.next() => match user_attributes_mutation { + Some(attributes) => self.handle_user_attributes_mutation(attributes), + None => { + error!("user attributes mutation selected, but none received"); + break; + } + }, cmd = async { commands.unwrap().recv().await }, if commands.is_some() => if let Some(cmd) = cmd { self.handle_command(cmd); }, @@ -573,6 +618,41 @@ impl SpircTask { } } + fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) { + trace!("Received attributes update: {:?}", update); + let attributes: UserAttributes = update + .get_pairs() + .iter() + .map(|pair| (pair.get_key().to_owned(), pair.get_value().to_owned())) + .collect(); + let _ = self.session.set_user_attributes(attributes); + } + + fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) { + for attribute in mutation.get_fields().iter() { + let key = attribute.get_name(); + if let Some(old_value) = self.session.user_attribute(key) { + let new_value = match old_value.as_ref() { + "0" => "1", + "1" => "0", + _ => &old_value, + }; + self.session.set_user_attribute(key, new_value); + trace!( + "Received attribute mutation, {} was {} is now {}", + key, + old_value, + new_value + ); + } else { + trace!( + "Received attribute mutation for {} but key was not found!", + key + ); + } + } + } + fn handle_frame(&mut self, frame: Frame) { let state_string = match frame.get_state().get_status() { PlayStatus::kPlayStatusLoading => "kPlayStatusLoading", diff --git a/core/Cargo.toml b/core/Cargo.toml index 4321c638..54fc1de7 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -36,6 +36,7 @@ once_cell = "1.5.2" pbkdf2 = { version = "0.8", default-features = false, features = ["hmac"] } priority-queue = "1.1" protobuf = "2.14.0" +quick-xml = { version = "0.22", features = [ "serialize" ] } rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 6cf3519e..841bd3d1 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -144,6 +144,27 @@ impl MercuryManager { } } + pub fn listen_for>( + &self, + uri: T, + ) -> impl Future> + 'static { + let uri = uri.into(); + + let manager = self.clone(); + async move { + let (tx, rx) = mpsc::unbounded_channel(); + + manager.lock(move |inner| { + if !inner.invalid { + debug!("listening to uri={}", uri); + inner.subscriptions.push((uri, tx)); + } + }); + + rx + } + } + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize; let seq = data.split_to(seq_len).as_ref().to_owned(); diff --git a/core/src/session.rs b/core/src/session.rs index f683960a..c1193dc3 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::future::Future; use std::io; use std::pin::Pin; @@ -13,6 +14,7 @@ use futures_core::TryStream; use futures_util::{future, ready, StreamExt, TryStreamExt}; use num_traits::FromPrimitive; use once_cell::sync::OnceCell; +use quick_xml::events::Event; use thiserror::Error; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -38,11 +40,14 @@ pub enum SessionError { IoError(#[from] io::Error), } +pub type UserAttributes = HashMap; + struct SessionData { country: String, time_delta: i64, canonical_username: String, invalid: bool, + user_attributes: UserAttributes, } struct SessionInternal { @@ -89,6 +94,7 @@ impl Session { canonical_username: String::new(), invalid: false, time_delta: 0, + user_attributes: HashMap::new(), }), http_client, tx_connection: sender_tx, @@ -224,11 +230,48 @@ impl Session { Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => { self.mercury().dispatch(packet_type.unwrap(), data); } + Some(ProductInfo) => { + let data = std::str::from_utf8(&data).unwrap(); + let mut reader = quick_xml::Reader::from_str(data); + + let mut buf = Vec::new(); + let mut current_element = String::new(); + let mut user_attributes: UserAttributes = HashMap::new(); + + loop { + match reader.read_event(&mut buf) { + Ok(Event::Start(ref element)) => { + current_element = + std::str::from_utf8(element.name()).unwrap().to_owned() + } + Ok(Event::End(_)) => { + current_element = String::new(); + } + Ok(Event::Text(ref value)) => { + if !current_element.is_empty() { + let _ = user_attributes.insert( + current_element.clone(), + value.unescape_and_decode(&reader).unwrap(), + ); + } + } + Ok(Event::Eof) => break, + Ok(_) => (), + Err(e) => error!( + "Error parsing XML at position {}: {:?}", + reader.buffer_position(), + e + ), + } + } + + trace!("Received product info: {:?}", user_attributes); + self.0.data.write().unwrap().user_attributes = user_attributes; + } Some(PongAck) | Some(SecretBlock) | Some(LegacyWelcome) | Some(UnknownDataAllZeros) - | Some(ProductInfo) | Some(LicenseVersion) => {} _ => { if let Some(packet_type) = PacketType::from_u8(cmd) { @@ -264,6 +307,38 @@ impl Session { &self.config().device_id } + pub fn user_attribute(&self, key: &str) -> Option { + self.0 + .data + .read() + .unwrap() + .user_attributes + .get(key) + .map(|value| value.to_owned()) + } + + pub fn user_attributes(&self) -> UserAttributes { + self.0.data.read().unwrap().user_attributes.clone() + } + + pub fn set_user_attribute(&self, key: &str, value: &str) -> Option { + self.0 + .data + .write() + .unwrap() + .user_attributes + .insert(key.to_owned(), value.to_owned()) + } + + pub fn set_user_attributes(&self, attributes: UserAttributes) { + self.0 + .data + .write() + .unwrap() + .user_attributes + .extend(attributes) + } + fn weak(&self) -> SessionWeak { SessionWeak(Arc::downgrade(&self.0)) } diff --git a/protocol/build.rs b/protocol/build.rs index 2a763183..a4ca4c37 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -26,6 +26,7 @@ fn compile() { proto_dir.join("playlist_annotate3.proto"), proto_dir.join("playlist_permission.proto"), proto_dir.join("playlist4_external.proto"), + proto_dir.join("user_attributes.proto"), // TODO: remove these legacy protobufs when we are on the new API completely proto_dir.join("authentication.proto"), proto_dir.join("canvaz.proto"), diff --git a/protocol/proto/user_attributes.proto b/protocol/proto/user_attributes.proto new file mode 100644 index 00000000..96ecf010 --- /dev/null +++ b/protocol/proto/user_attributes.proto @@ -0,0 +1,29 @@ +// Custom protobuf crafted from spotify:user:attributes:mutated response: +// +// 1 { +// 1: "filter-explicit-content" +// } +// 2 { +// 1: 1639087299 +// 2: 418909000 +// } + +syntax = "proto3"; + +package spotify.user_attributes.proto; + +option optimize_for = CODE_SIZE; + +message UserAttributesMutation { + repeated MutatedField fields = 1; + MutationCommand cmd = 2; +} + +message MutatedField { + string name = 1; +} + +message MutationCommand { + int64 timestamp = 1; + int32 unknown = 2; +} From 51b6c46fcdabd0d614c3615dbb985d14f051f6b5 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 11 Dec 2021 16:43:34 +0100 Subject: [PATCH 66/95] Receive autoplay and other attributes --- core/src/connection/handshake.rs | 9 +++++++-- core/src/http_client.rs | 10 ++++------ core/src/version.rs | 6 ++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 3659ab82..8acc0d01 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -14,6 +14,7 @@ use crate::protocol; use crate::protocol::keyexchange::{ APResponseMessage, ClientHello, ClientResponsePlaintext, Platform, ProductFlags, }; +use crate::version; pub async fn handshake( mut connection: T, @@ -84,13 +85,17 @@ where let mut packet = ClientHello::new(); packet .mut_build_info() - .set_product(protocol::keyexchange::Product::PRODUCT_LIBSPOTIFY); + // ProductInfo won't push autoplay and perhaps other settings + // when set to anything else than PRODUCT_CLIENT + .set_product(protocol::keyexchange::Product::PRODUCT_CLIENT); packet .mut_build_info() .mut_product_flags() .push(PRODUCT_FLAGS); packet.mut_build_info().set_platform(platform); - packet.mut_build_info().set_version(999999999); + packet + .mut_build_info() + .set_version(version::SPOTIFY_VERSION); packet .mut_cryptosuites_supported() .push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON); diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 157fbaef..7b8aad72 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -9,7 +9,7 @@ use std::env::consts::OS; use thiserror::Error; use url::Url; -use crate::version; +use crate::version::{SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING}; pub struct HttpClient { proxy: Option, @@ -54,8 +54,8 @@ impl HttpClient { let connector = HttpsConnector::with_native_roots(); let spotify_version = match OS { - "android" | "ios" => "8.6.84", - _ => "117300517", + "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), + _ => SPOTIFY_VERSION.to_string(), }; let spotify_platform = match OS { @@ -72,9 +72,7 @@ impl HttpClient { // Some features like lyrics are version-gated and require an official version string. HeaderValue::from_str(&format!( "Spotify/{} {} ({})", - spotify_version, - spotify_platform, - version::VERSION_STRING + spotify_version, spotify_platform, VERSION_STRING ))?, ); diff --git a/core/src/version.rs b/core/src/version.rs index ef553463..a7e3acd9 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -15,3 +15,9 @@ pub const SEMVER: &str = env!("CARGO_PKG_VERSION"); /// A random build id. pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID"); + +/// The protocol version of the Spotify desktop client. +pub const SPOTIFY_VERSION: u64 = 117300517; + +/// The protocol version of the Spotify mobile app. +pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84"; From e748d543e9224ffb62c046b5d0f38d7ca8683caa Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 11 Dec 2021 20:22:44 +0100 Subject: [PATCH 67/95] Check availability from the catalogue attribute --- connect/src/spirc.rs | 9 +++-- core/src/session.rs | 74 ++++++++++++++++++++++-------------- metadata/src/audio/item.rs | 25 ++++++++---- metadata/src/availability.rs | 2 + metadata/src/episode.rs | 4 +- metadata/src/track.rs | 6 ++- 6 files changed, 76 insertions(+), 44 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index bc596bfc..7b9b0857 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -237,8 +237,9 @@ impl Spirc { let ident = session.device_id().to_owned(); // Uri updated in response to issue #288 - debug!("canonical_username: {}", &session.username()); - let uri = format!("hm://remote/user/{}/", url_encode(&session.username())); + let canonical_username = &session.username(); + debug!("canonical_username: {}", canonical_username); + let uri = format!("hm://remote/user/{}/", url_encode(canonical_username)); let subscription = Box::pin( session @@ -631,11 +632,11 @@ impl SpircTask { fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) { for attribute in mutation.get_fields().iter() { let key = attribute.get_name(); - if let Some(old_value) = self.session.user_attribute(key) { + if let Some(old_value) = self.session.user_data().attributes.get(key) { let new_value = match old_value.as_ref() { "0" => "1", "1" => "0", - _ => &old_value, + _ => old_value, }; self.session.set_user_attribute(key, new_value); trace!( diff --git a/core/src/session.rs b/core/src/session.rs index c1193dc3..926c4bc1 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::future::Future; use std::io; use std::pin::Pin; +use std::process::exit; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, RwLock, Weak}; use std::task::Context; @@ -42,12 +43,18 @@ pub enum SessionError { pub type UserAttributes = HashMap; +#[derive(Debug, Clone, Default)] +pub struct UserData { + pub country: String, + pub canonical_username: String, + pub attributes: UserAttributes, +} + +#[derive(Debug, Clone, Default)] struct SessionData { - country: String, time_delta: i64, - canonical_username: String, invalid: bool, - user_attributes: UserAttributes, + user_data: UserData, } struct SessionInternal { @@ -89,13 +96,7 @@ impl Session { let session = Session(Arc::new(SessionInternal { config, - data: RwLock::new(SessionData { - country: String::new(), - canonical_username: String::new(), - invalid: false, - time_delta: 0, - user_attributes: HashMap::new(), - }), + data: RwLock::new(SessionData::default()), http_client, tx_connection: sender_tx, cache: cache.map(Arc::new), @@ -118,7 +119,8 @@ impl Session { connection::authenticate(&mut transport, credentials, &session.config().device_id) .await?; info!("Authenticated as \"{}\" !", reusable_credentials.username); - session.0.data.write().unwrap().canonical_username = reusable_credentials.username.clone(); + session.0.data.write().unwrap().user_data.canonical_username = + reusable_credentials.username.clone(); if let Some(cache) = session.cache() { cache.save_credentials(&reusable_credentials); } @@ -199,6 +201,18 @@ impl Session { ); } + fn check_catalogue(attributes: &UserAttributes) { + if let Some(account_type) = attributes.get("type") { + if account_type != "premium" { + error!("librespot does not support {:?} accounts.", account_type); + info!("Please support Spotify and your artists and sign up for a premium account."); + + // TODO: logout instead of exiting + exit(1); + } + } + } + fn dispatch(&self, cmd: u8, data: Bytes) { use PacketType::*; let packet_type = FromPrimitive::from_u8(cmd); @@ -219,7 +233,7 @@ impl Session { Some(CountryCode) => { let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); info!("Country: {:?}", country); - self.0.data.write().unwrap().country = country; + self.0.data.write().unwrap().user_data.country = country; } Some(StreamChunkRes) | Some(ChannelError) => { self.channel().dispatch(packet_type.unwrap(), data); @@ -266,7 +280,9 @@ impl Session { } trace!("Received product info: {:?}", user_attributes); - self.0.data.write().unwrap().user_attributes = user_attributes; + Self::check_catalogue(&user_attributes); + + self.0.data.write().unwrap().user_data.attributes = user_attributes; } Some(PongAck) | Some(SecretBlock) @@ -295,47 +311,47 @@ impl Session { &self.0.config } - pub fn username(&self) -> String { - self.0.data.read().unwrap().canonical_username.clone() - } - - pub fn country(&self) -> String { - self.0.data.read().unwrap().country.clone() + pub fn user_data(&self) -> UserData { + self.0.data.read().unwrap().user_data.clone() } pub fn device_id(&self) -> &str { &self.config().device_id } - pub fn user_attribute(&self, key: &str) -> Option { + pub fn username(&self) -> String { self.0 .data .read() .unwrap() - .user_attributes - .get(key) - .map(|value| value.to_owned()) - } - - pub fn user_attributes(&self) -> UserAttributes { - self.0.data.read().unwrap().user_attributes.clone() + .user_data + .canonical_username + .clone() } pub fn set_user_attribute(&self, key: &str, value: &str) -> Option { + let mut dummy_attributes = UserAttributes::new(); + dummy_attributes.insert(key.to_owned(), value.to_owned()); + Self::check_catalogue(&dummy_attributes); + self.0 .data .write() .unwrap() - .user_attributes + .user_data + .attributes .insert(key.to_owned(), value.to_owned()) } pub fn set_user_attributes(&self, attributes: UserAttributes) { + Self::check_catalogue(&attributes); + self.0 .data .write() .unwrap() - .user_attributes + .user_data + .attributes .extend(attributes) } diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index 09b72ebc..50aa2bf9 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -12,7 +12,7 @@ use crate::{ use super::file::AudioFiles; -use librespot_core::session::Session; +use librespot_core::session::{Session, UserData}; use librespot_core::spotify_id::{SpotifyId, SpotifyItemType}; pub type AudioItemResult = Result; @@ -43,18 +43,27 @@ impl AudioItem { pub trait InnerAudioItem { async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult; - fn allowed_in_country(restrictions: &Restrictions, country: &str) -> AudioItemAvailability { + fn allowed_for_user( + user_data: &UserData, + restrictions: &Restrictions, + ) -> AudioItemAvailability { + let country = &user_data.country; + let user_catalogue = match user_data.attributes.get("catalogue") { + Some(catalogue) => catalogue, + None => "premium", + }; + for premium_restriction in restrictions.iter().filter(|restriction| { restriction .catalogue_strs .iter() - .any(|catalogue| *catalogue == "premium") + .any(|restricted_catalogue| restricted_catalogue == user_catalogue) }) { if let Some(allowed_countries) = &premium_restriction.countries_allowed { // A restriction will specify either a whitelast *or* a blacklist, // but not both. So restrict availability if there is a whitelist // and the country isn't on it. - if allowed_countries.iter().any(|allowed| country == *allowed) { + if allowed_countries.iter().any(|allowed| country == allowed) { return Ok(()); } else { return Err(UnavailabilityReason::NotWhitelisted); @@ -64,7 +73,7 @@ pub trait InnerAudioItem { if let Some(forbidden_countries) = &premium_restriction.countries_forbidden { if forbidden_countries .iter() - .any(|forbidden| country == *forbidden) + .any(|forbidden| country == forbidden) { return Err(UnavailabilityReason::Blacklisted); } else { @@ -92,13 +101,13 @@ pub trait InnerAudioItem { Ok(()) } - fn available_in_country( + fn available_for_user( + user_data: &UserData, availability: &Availabilities, restrictions: &Restrictions, - country: &str, ) -> AudioItemAvailability { Self::available(availability)?; - Self::allowed_in_country(restrictions, country)?; + Self::allowed_for_user(user_data, restrictions)?; Ok(()) } } diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index c40427cb..eb2b5fdd 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -33,6 +33,8 @@ pub enum UnavailabilityReason { Blacklisted, #[error("available date is in the future")] Embargo, + #[error("required data was not present")] + NoData, #[error("whitelist present and country not on it")] NotWhitelisted, } diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 30c89f19..7032999b 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -67,10 +67,10 @@ impl Deref for Episodes { impl InnerAudioItem for Episode { async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult { let episode = Self::get(session, id).await?; - let availability = Self::available_in_country( + let availability = Self::available_for_user( + &session.user_data(), &episode.availability, &episode.restrictions, - &session.country(), ); Ok(AudioItem { diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 8e7f6702..d0639c82 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -81,7 +81,11 @@ impl InnerAudioItem for Track { let availability = if Local::now() < track.earliest_live_timestamp.as_utc() { Err(UnavailabilityReason::Embargo) } else { - Self::available_in_country(&track.availability, &track.restrictions, &session.country()) + Self::available_for_user( + &session.user_data(), + &track.availability, + &track.restrictions, + ) }; Ok(AudioItem { From 9a31aa03622ccacaf36d523e746ef7e1a39bf07e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 11 Dec 2021 20:45:08 +0100 Subject: [PATCH 68/95] Pretty-print trace messages --- connect/src/spirc.rs | 2 +- core/src/session.rs | 4 ++-- core/src/token.rs | 2 +- metadata/src/lib.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 7b9b0857..e0b817ec 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -620,7 +620,7 @@ impl SpircTask { } fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) { - trace!("Received attributes update: {:?}", update); + trace!("Received attributes update: {:#?}", update); let attributes: UserAttributes = update .get_pairs() .iter() diff --git a/core/src/session.rs b/core/src/session.rs index 926c4bc1..ed9609d7 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -279,7 +279,7 @@ impl Session { } } - trace!("Received product info: {:?}", user_attributes); + trace!("Received product info: {:#?}", user_attributes); Self::check_catalogue(&user_attributes); self.0.data.write().unwrap().user_data.attributes = user_attributes; @@ -291,7 +291,7 @@ impl Session { | Some(LicenseVersion) => {} _ => { if let Some(packet_type) = PacketType::from_u8(cmd) { - trace!("Ignoring {:?} packet with data {:?}", packet_type, data); + trace!("Ignoring {:?} packet with data {:#?}", packet_type, data); } else { trace!("Ignoring unknown packet {:x}", cmd); } diff --git a/core/src/token.rs b/core/src/token.rs index 91a395fd..b9afa620 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -87,7 +87,7 @@ impl TokenProvider { .expect("No tokens received") .to_vec(); let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?; - trace!("Got token: {:?}", token); + trace!("Got token: {:#?}", token); self.lock(|inner| inner.tokens.push(token.clone())); Ok(token) } diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 3f1849b5..15b68e1f 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -50,7 +50,7 @@ pub trait Metadata: Send + Sized + 'static { async fn get(session: &Session, id: SpotifyId) -> Result { let response = Self::request(session, id).await?; let msg = Self::Message::parse_from_bytes(&response)?; - trace!("Received metadata: {:?}", msg); + trace!("Received metadata: {:#?}", msg); Self::parse(&msg, id) } From 9a93cca562581d9e12f9af16efed5060a34d0dc6 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 11 Dec 2021 23:06:58 +0100 Subject: [PATCH 69/95] Get connection ID --- connect/src/spirc.rs | 29 +++++++++++++++++++++++++++++ core/src/mercury/mod.rs | 1 + core/src/session.rs | 9 +++++++++ 3 files changed, 39 insertions(+) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index e0b817ec..b3878a42 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -61,6 +61,7 @@ struct SpircTask { play_status: SpircPlayStatus, subscription: BoxedStream, + connection_id_update: BoxedStream, user_attributes_update: BoxedStream, user_attributes_mutation: BoxedStream, sender: MercurySender, @@ -254,6 +255,21 @@ impl Spirc { }), ); + let connection_id_update = Box::pin( + session + .mercury() + .listen_for("hm://pusher/v1/connections/") + .map(UnboundedReceiverStream::new) + .flatten_stream() + .map(|response| -> String { + response + .uri + .strip_prefix("hm://pusher/v1/connections/") + .unwrap_or("") + .to_owned() + }), + ); + let user_attributes_update = Box::pin( session .mercury() @@ -306,6 +322,7 @@ impl Spirc { play_status: SpircPlayStatus::Stopped, subscription, + connection_id_update, user_attributes_update, user_attributes_mutation, sender, @@ -390,6 +407,13 @@ impl SpircTask { break; } }, + connection_id_update = self.connection_id_update.next() => match connection_id_update { + Some(connection_id) => self.handle_connection_id_update(connection_id), + None => { + error!("connection ID update selected, but none received"); + break; + } + }, cmd = async { commands.unwrap().recv().await }, if commands.is_some() => if let Some(cmd) = cmd { self.handle_command(cmd); }, @@ -619,6 +643,11 @@ impl SpircTask { } } + fn handle_connection_id_update(&mut self, connection_id: String) { + trace!("Received connection ID update: {:?}", connection_id); + self.session.set_connection_id(connection_id); + } + fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) { trace!("Received attributes update: {:#?}", update); let attributes: UserAttributes = update diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 841bd3d1..ad2d5013 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -264,6 +264,7 @@ impl MercuryManager { if !found { debug!("unknown subscription uri={}", response.uri); + trace!("response pushed over Mercury: {:?}", response); } }) } else if let Some(cb) = pending.callback { diff --git a/core/src/session.rs b/core/src/session.rs index ed9609d7..426480f6 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -52,6 +52,7 @@ pub struct UserData { #[derive(Debug, Clone, Default)] struct SessionData { + connection_id: String, time_delta: i64, invalid: bool, user_data: UserData, @@ -319,6 +320,14 @@ impl Session { &self.config().device_id } + pub fn connection_id(&self) -> String { + self.0.data.read().unwrap().connection_id.clone() + } + + pub fn set_connection_id(&self, connection_id: String) { + self.0.data.write().unwrap().connection_id = connection_id; + } + pub fn username(&self) -> String { self.0 .data From 8c480b7e39e911ddbfc4d564932d09dbdd7b6af6 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sun, 5 Dec 2021 13:00:33 -0600 Subject: [PATCH 70/95] Fix Command line arguments incorrectly echoed in TRACE Fix up for #886 Closes: #898 And... * Don't silently ignore non-Unicode while parsing env vars. * Iterating over `std::env::args` will panic! on invalid unicode. Let's not do that. `getopts` will catch missing args and exit if those args are required after our error message about the arg not being valid unicode. * Gaurd against empty strings. There are a few places while parsing options strings that we don't immediately evaluate their validity let's at least makes sure that they are not empty if present. * `args` is only used in `get_setup` it doesn't need to be in main. * Nicer help header. * Get rid of `use std::io::{stderr, Write};` and just use `rpassword::prompt_password_stderr`. * Get rid of `get_credentials` it was clunky, ugly and only used once. There is no need for it to be a separate function. * Handle an empty password prompt and password prompt parsing errors. * + Other random misc clean ups. --- src/main.rs | 415 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 240 insertions(+), 175 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2dec56ae..789654ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,6 @@ mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; use std::env; -use std::io::{stderr, Write}; use std::ops::RangeInclusive; use std::path::Path; use std::pin::Pin; @@ -40,27 +39,16 @@ fn device_id(name: &str) -> String { } fn usage(program: &str, opts: &getopts::Options) -> String { - let brief = format!("Usage: {} [options]", program); + let repo_home = env!("CARGO_PKG_REPOSITORY"); + let desc = env!("CARGO_PKG_DESCRIPTION"); + let version = get_version_string(); + let brief = format!( + "{}\n\n{}\n\n{}\n\nUsage: {} []", + version, desc, repo_home, program + ); opts.usage(&brief) } -fn arg_to_var(arg: &str) -> String { - // To avoid name collisions environment variables must be prepended - // with `LIBRESPOT_` so option/flag `foo-bar` becomes `LIBRESPOT_FOO_BAR`. - format!("LIBRESPOT_{}", arg.to_uppercase().replace("-", "_")) -} - -fn env_var_present(arg: &str) -> bool { - env::var(arg_to_var(arg)).is_ok() -} - -fn env_var_opt_str(option: &str) -> Option { - match env::var(arg_to_var(option)) { - Ok(value) => Some(value), - Err(_) => None, - } -} - fn setup_logging(quiet: bool, verbose: bool) { let mut builder = env_logger::Builder::new(); match env::var("RUST_LOG") { @@ -102,29 +90,6 @@ fn list_backends() { } } -pub fn get_credentials Option>( - username: Option, - password: Option, - cached_credentials: Option, - prompt: F, -) -> Option { - if let Some(username) = username { - if let Some(password) = password { - return Some(Credentials::with_password(username, password)); - } - - match cached_credentials { - Some(credentials) if username == credentials.username => Some(credentials), - _ => { - let password = prompt(&username)?; - Some(Credentials::with_password(username, password)) - } - } - } else { - cached_credentials - } -} - #[derive(Debug, Error)] pub enum ParseFileSizeError { #[error("empty argument")] @@ -218,9 +183,10 @@ struct Setup { emit_sink_events: bool, } -fn get_setup(args: &[String]) -> Setup { - const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; +fn get_setup() -> Setup { + const VALID_INITIAL_VOLUME_RANGE: RangeInclusive = 0..=100; const VALID_VOLUME_RANGE: RangeInclusive = 0.0..=100.0; + const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive = -10.0..=0.0; const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive = 1..=500; @@ -596,25 +562,72 @@ fn get_setup(args: &[String]) -> Setup { "PORT", ); + let args: Vec<_> = std::env::args_os() + .filter_map(|s| match s.into_string() { + Ok(valid) => Some(valid), + Err(s) => { + eprintln!( + "Command line argument was not valid Unicode and will not be evaluated: {:?}", + s + ); + None + } + }) + .collect(); + let matches = match opts.parse(&args[1..]) { Ok(m) => m, - Err(f) => { - eprintln!( - "Error parsing command line options: {}\n{}", - f, - usage(&args[0], &opts) - ); + Err(e) => { + eprintln!("Error parsing command line options: {}", e); + println!("\n{}", usage(&args[0], &opts)); exit(1); } }; - let opt_present = |opt| matches.opt_present(opt) || env_var_present(opt); + let stripped_env_key = |k: &str| { + k.trim_start_matches("LIBRESPOT_") + .replace("_", "-") + .to_lowercase() + }; + + let env_vars: Vec<_> = env::vars_os().filter_map(|(k, v)| { + let mut env_var = None; + if let Ok(key) = k.into_string() { + if key.starts_with("LIBRESPOT_") { + let stripped_key = stripped_env_key(&key); + // Only match against long option/flag names. + // Something like LIBRESPOT_V for example is + // not valid because there are both -v and -V flags + // but env vars are assumed to be all uppercase. + let len = stripped_key.chars().count(); + if len > 1 && matches.opt_defined(&stripped_key) { + match v.into_string() { + Ok(value) => { + env_var = Some((key, value)); + }, + Err(s) => { + eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s); + } + } + } + } + } + + env_var + }) + .collect(); + + let opt_present = + |opt| matches.opt_present(opt) || env_vars.iter().any(|(k, _)| stripped_env_key(k) == opt); let opt_str = |opt| { if matches.opt_present(opt) { matches.opt_str(opt) } else { - env_var_opt_str(opt) + env_vars + .iter() + .find(|(k, _)| stripped_env_key(k) == opt) + .map(|(_, v)| v.to_string()) } }; @@ -632,55 +645,43 @@ fn get_setup(args: &[String]) -> Setup { info!("{}", get_version_string()); - let librespot_env_vars: Vec = env::vars_os() - .filter_map(|(k, v)| { - let mut env_var = None; - if let Some(key) = k.to_str() { - if key.starts_with("LIBRESPOT_") { - if matches!(key, "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { - // Don't log creds. - env_var = Some(format!("\t\t{}=XXXXXXXX", key)); - } else if let Some(value) = v.to_str() { - env_var = Some(format!("\t\t{}={}", key, value)); - } - } - } - - env_var - }) - .collect(); - - if !librespot_env_vars.is_empty() { + if !env_vars.is_empty() { trace!("Environment variable(s):"); - for kv in librespot_env_vars { - trace!("{}", kv); + for (k, v) in &env_vars { + if matches!(k.as_str(), "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { + trace!("\t\t{}=\"XXXXXXXX\"", k); + } else if v.is_empty() { + trace!("\t\t{}=", k); + } else { + trace!("\t\t{}=\"{}\"", k, v); + } } } - let cmd_args = &args[1..]; + let args_len = args.len(); - let cmd_args_len = cmd_args.len(); - - if cmd_args_len > 0 { + if args_len > 1 { trace!("Command line argument(s):"); - for (index, key) in cmd_args.iter().enumerate() { - if key.starts_with('-') || key.starts_with("--") { - if matches!(key.as_str(), "--password" | "-p" | "--username" | "-u") { - // Don't log creds. - trace!("\t\t{} XXXXXXXX", key); - } else { - let mut value = "".to_string(); - let next = index + 1; - if next < cmd_args_len { - let next_key = cmd_args[next].clone(); - if !next_key.starts_with('-') && !next_key.starts_with("--") { - value = next_key; - } - } + for (index, key) in args.iter().enumerate() { + let opt = key.trim_start_matches('-'); - trace!("\t\t{} {}", key, value); + if index > 0 + && &args[index - 1] != key + && matches.opt_defined(opt) + && matches.opt_present(opt) + { + if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) { + // Don't log creds. + trace!("\t\t{} \"XXXXXXXX\"", key); + } else { + let value = matches.opt_str(opt).unwrap_or_else(|| "".to_string()); + if value.is_empty() { + trace!("\t\t{}", key); + } else { + trace!("\t\t{} \"{}\"", key, value); + } } } } @@ -707,7 +708,7 @@ fn get_setup(args: &[String]) -> Setup { let backend = audio_backend::find(backend_name).unwrap_or_else(|| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", BACKEND, BACKEND_SHORT, opt_str(BACKEND).unwrap_or_default() @@ -720,7 +721,10 @@ fn get_setup(args: &[String]) -> Setup { .as_deref() .map(|format| { AudioFormat::from_str(format).unwrap_or_else(|_| { - error!("Invalid `--{}` / `-{}`: {}", FORMAT, FORMAT_SHORT, format); + error!( + "Invalid `--{}` / `-{}`: \"{}\"", + FORMAT, FORMAT_SHORT, format + ); println!( "Valid `--{}` / `-{}` values: F64, F32, S32, S24, S24_3, S16", FORMAT, FORMAT_SHORT @@ -743,9 +747,17 @@ fn get_setup(args: &[String]) -> Setup { feature = "rodio-backend", feature = "portaudio-backend" ))] - if device == Some("?".into()) { - backend(device, format); - exit(0); + if let Some(ref value) = device { + if value == "?" { + backend(device, format); + exit(0); + } else if value.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + DEVICE, DEVICE_SHORT + ); + exit(1); + } } #[cfg(not(any( @@ -774,7 +786,7 @@ fn get_setup(args: &[String]) -> Setup { let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", MIXER_TYPE, MIXER_TYPE_SHORT, opt_str(MIXER_TYPE).unwrap_or_default() @@ -807,7 +819,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|index| { index.parse::().unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", ALSA_MIXER_INDEX, ALSA_MIXER_INDEX_SHORT, index ); println!("Default: {}", mixer_default_config.index); @@ -822,6 +834,15 @@ fn get_setup(args: &[String]) -> Setup { #[cfg(feature = "alsa-backend")] let control = opt_str(ALSA_MIXER_CONTROL).unwrap_or(mixer_default_config.control); + #[cfg(feature = "alsa-backend")] + if control.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + ALSA_MIXER_CONTROL, ALSA_MIXER_CONTROL_SHORT + ); + exit(1); + } + #[cfg(not(feature = "alsa-backend"))] let control = mixer_default_config.control; @@ -829,7 +850,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|range| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", VOLUME_RANGE, VOLUME_RANGE_SHORT, range ); println!( @@ -871,7 +892,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|volume_ctrl| { VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", VOLUME_CTRL, VOLUME_CTRL_SHORT, volume_ctrl ); println!( @@ -918,7 +939,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|e| { e.unwrap_or_else(|e| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT, e ); exit(1); @@ -945,20 +966,57 @@ fn get_setup(args: &[String]) -> Setup { }; let credentials = { - let cached_credentials = cache.as_ref().and_then(Cache::credentials); + let cached_creds = cache.as_ref().and_then(Cache::credentials); - let password = |username: &String| -> Option { - write!(stderr(), "Password for {}: ", username).ok()?; - stderr().flush().ok()?; - rpassword::read_password().ok() - }; + if let Some(username) = opt_str(USERNAME) { + if username.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + USERNAME, USERNAME_SHORT + ); + exit(1); + } + if let Some(password) = opt_str(PASSWORD) { + if password.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + PASSWORD, PASSWORD_SHORT + ); + exit(1); + } + Some(Credentials::with_password(username, password)) + } else { + match cached_creds { + Some(creds) if username == creds.username => Some(creds), + _ => { + let prompt = &format!("Password for {}: ", username); - get_credentials( - opt_str(USERNAME), - opt_str(PASSWORD), - cached_credentials, - password, - ) + match rpassword::prompt_password_stderr(prompt) { + Ok(password) => { + if !password.is_empty() { + Some(Credentials::with_password(username, password)) + } else { + trace!("Password was empty."); + if cached_creds.is_some() { + trace!("Using cached credentials."); + } + cached_creds + } + } + Err(e) => { + warn!("Cannot parse password: {}", e); + if cached_creds.is_some() { + trace!("Using cached credentials."); + } + cached_creds + } + } + } + } + } + } else { + cached_creds + } }; let enable_discovery = !opt_present(DISABLE_DISCOVERY); @@ -980,12 +1038,14 @@ fn get_setup(args: &[String]) -> Setup { .map(|port| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", ZEROCONF_PORT, ZEROCONF_PORT_SHORT, port ); println!( - "Valid `--{}` / `-{}` values: 1 - 65535", - ZEROCONF_PORT, ZEROCONF_PORT_SHORT + "Valid `--{}` / `-{}` values: 1 - {}", + ZEROCONF_PORT, + ZEROCONF_PORT_SHORT, + u16::MAX ); }; @@ -1011,16 +1071,27 @@ fn get_setup(args: &[String]) -> Setup { let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); + if name.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + NAME, NAME_SHORT + ); + exit(1); + } + let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", INITIAL_VOLUME, INITIAL_VOLUME_SHORT, initial_volume ); println!( - "Valid `--{}` / `-{}` values: 0 - 100", - INITIAL_VOLUME, INITIAL_VOLUME_SHORT + "Valid `--{}` / `-{}` values: {} - {}", + INITIAL_VOLUME, + INITIAL_VOLUME_SHORT, + VALID_INITIAL_VOLUME_RANGE.start(), + VALID_INITIAL_VOLUME_RANGE.end() ); #[cfg(feature = "alsa-backend")] println!( @@ -1039,7 +1110,7 @@ fn get_setup(args: &[String]) -> Setup { exit(1); }); - if volume > 100 { + if !(VALID_INITIAL_VOLUME_RANGE).contains(&volume) { on_error(); exit(1); } @@ -1056,7 +1127,7 @@ fn get_setup(args: &[String]) -> Setup { .as_deref() .map(|device_type| { DeviceType::from_str(device_type).unwrap_or_else(|_| { - error!("Invalid `--{}` / `-{}`: {}", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); + error!("Invalid `--{}` / `-{}`: \"{}\"", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); println!("Valid `--{}` / `-{}` values: computer, tablet, smartphone, speaker, tv, avr, stb, audiodongle, \ gameconsole, castaudio, castvideo, automobile, smartwatch, chromebook, carthing, homething", DEVICE_TYPE, DEVICE_TYPE_SHORT @@ -1079,55 +1150,50 @@ fn get_setup(args: &[String]) -> Setup { } }; - let session_config = { - let device_id = device_id(&connect_config.name); - - SessionConfig { - user_agent: version::VERSION_STRING.to_string(), - device_id, - proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( - |s| { - match Url::parse(&s) { - Ok(url) => { - if url.host().is_none() || url.port_or_known_default().is_none() { - error!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); - exit(1); - } - - if url.scheme() != "http" { - error!("Only unsecure http:// proxies are supported"); - exit(1); - } - - url - }, - Err(e) => { - error!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", e); + let session_config = SessionConfig { + user_agent: version::VERSION_STRING.to_string(), + device_id: device_id(&connect_config.name), + proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( + |s| { + match Url::parse(&s) { + Ok(url) => { + if url.host().is_none() || url.port_or_known_default().is_none() { + error!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); exit(1); } - } - }, - ), - ap_port: opt_str(AP_PORT) - .map(|port| { - let on_error = || { - error!("Invalid `--{}` / `-{}`: {}", AP_PORT, AP_PORT_SHORT, port); - println!("Valid `--{}` / `-{}` values: 1 - 65535", AP_PORT, AP_PORT_SHORT); - }; - let port = port.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); + if url.scheme() != "http" { + error!("Only unsecure http:// proxies are supported"); + exit(1); + } - if port == 0 { - on_error(); + url + }, + Err(e) => { + error!("Invalid proxy URL: \"{}\", only URLs in the format \"http://host:port\" are allowed", e); exit(1); } + } + }, + ), + ap_port: opt_str(AP_PORT).map(|port| { + let on_error = || { + error!("Invalid `--{}` / `-{}`: \"{}\"", AP_PORT, AP_PORT_SHORT, port); + println!("Valid `--{}` / `-{}` values: 1 - {}", AP_PORT, AP_PORT_SHORT, u16::MAX); + }; - port - }), - } + let port = port.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if port == 0 { + on_error(); + exit(1); + } + + port + }), }; let player_config = { @@ -1138,7 +1204,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|bitrate| { Bitrate::from_str(bitrate).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", BITRATE, BITRATE_SHORT, bitrate ); println!( @@ -1200,7 +1266,7 @@ fn get_setup(args: &[String]) -> Setup { let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method ); println!( @@ -1227,7 +1293,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|gain_type| { NormalisationType::from_str(gain_type).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, gain_type ); println!( @@ -1244,7 +1310,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|pregain| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_PREGAIN, NORMALISATION_PREGAIN_SHORT, pregain ); println!( @@ -1275,7 +1341,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|threshold| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_THRESHOLD, NORMALISATION_THRESHOLD_SHORT, threshold ); println!( @@ -1309,7 +1375,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|attack| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_ATTACK, NORMALISATION_ATTACK_SHORT, attack ); println!( @@ -1343,7 +1409,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|release| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_RELEASE, NORMALISATION_RELEASE_SHORT, release ); println!( @@ -1377,7 +1443,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|knee| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_KNEE, NORMALISATION_KNEE_SHORT, knee ); println!( @@ -1418,7 +1484,7 @@ fn get_setup(args: &[String]) -> Setup { Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", DITHER, DITHER_SHORT, opt_str(DITHER).unwrap_or_default() @@ -1488,8 +1554,7 @@ async fn main() { env::set_var(RUST_BACKTRACE, "full") } - let args: Vec = std::env::args().collect(); - let setup = get_setup(&args); + let setup = get_setup(); let mut last_credentials = None; let mut spirc: Option = None; From 79c4040a53f50f14f27c6d9e0fec9ad00101f638 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 11 Dec 2021 21:32:34 +0100 Subject: [PATCH 71/95] Skip track on decoding error --- playback/src/player.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index a7ff916d..a56130f3 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -737,7 +737,14 @@ impl PlayerTrackLoader { } }; - assert!(audio.duration >= 0); + if audio.duration < 0 { + error!( + "Track duration for <{}> cannot be {}", + spotify_id.to_uri(), + audio.duration + ); + return None; + } let duration_ms = audio.duration as u32; // (Most) podcasts seem to support only 96 bit Vorbis, so fall back to it @@ -945,7 +952,7 @@ impl Future for PlayerInternal { } Poll::Ready(Err(_)) => { warn!("Unable to load <{:?}>\nSkipping to next track", track_id); - assert!(self.state.is_loading()); + debug_assert!(self.state.is_loading()); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, @@ -1045,8 +1052,11 @@ impl Future for PlayerInternal { } } Err(e) => { - error!("PlayerInternal poll: {}", e); - exit(1); + warn!("Unable to decode samples for track <{:?}: {:?}>\nSkipping to next track", track_id, e); + self.send_event(PlayerEvent::EndOfTrack { + track_id, + play_request_id, + }) } } } @@ -1058,8 +1068,15 @@ impl Future for PlayerInternal { self.handle_packet(packet, normalisation_factor); } Err(e) => { - error!("PlayerInternal poll: {}", e); - exit(1); + warn!( + "Unable to get packet for track <{:?}: {:?}>\nSkipping to next track", + e, + track_id + ); + self.send_event(PlayerEvent::EndOfTrack { + track_id, + play_request_id, + }) } } } else { From 8f23c3498fd9eeaf7d74ab2cb57548f0e811de57 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 12 Dec 2021 20:01:05 +0100 Subject: [PATCH 72/95] Clean up warnings --- playback/src/player.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index a56130f3..d8dbb190 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -950,8 +950,11 @@ impl Future for PlayerInternal { exit(1); } } - Poll::Ready(Err(_)) => { - warn!("Unable to load <{:?}>\nSkipping to next track", track_id); + Poll::Ready(Err(e)) => { + warn!( + "Skipping to next track, unable to load track <{:?}>: {:?}", + track_id, e + ); debug_assert!(self.state.is_loading()); self.send_event(PlayerEvent::EndOfTrack { track_id, @@ -1052,7 +1055,7 @@ impl Future for PlayerInternal { } } Err(e) => { - warn!("Unable to decode samples for track <{:?}: {:?}>\nSkipping to next track", track_id, e); + warn!("Skipping to next track, unable to decode samples for track <{:?}>: {:?}", track_id, e); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, @@ -1068,11 +1071,7 @@ impl Future for PlayerInternal { self.handle_packet(packet, normalisation_factor); } Err(e) => { - warn!( - "Unable to get packet for track <{:?}: {:?}>\nSkipping to next track", - e, - track_id - ); + warn!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, From d29337c62df4e12e8d2120ab973b50ec6ab75a38 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sun, 12 Dec 2021 17:28:41 -0600 Subject: [PATCH 73/95] Dry up error messages. --- src/main.rs | 406 +++++++++++++++++++++++++--------------------------- 1 file changed, 199 insertions(+), 207 deletions(-) diff --git a/src/main.rs b/src/main.rs index 789654ff..7e13a323 100644 --- a/src/main.rs +++ b/src/main.rs @@ -590,30 +590,23 @@ fn get_setup() -> Setup { .to_lowercase() }; - let env_vars: Vec<_> = env::vars_os().filter_map(|(k, v)| { - let mut env_var = None; - if let Ok(key) = k.into_string() { - if key.starts_with("LIBRESPOT_") { - let stripped_key = stripped_env_key(&key); - // Only match against long option/flag names. - // Something like LIBRESPOT_V for example is - // not valid because there are both -v and -V flags - // but env vars are assumed to be all uppercase. - let len = stripped_key.chars().count(); - if len > 1 && matches.opt_defined(&stripped_key) { - match v.into_string() { - Ok(value) => { - env_var = Some((key, value)); - }, - Err(s) => { - eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s); - } + let env_vars: Vec<_> = env::vars_os().filter_map(|(k, v)| match k.into_string() { + Ok(key) if key.starts_with("LIBRESPOT_") => { + let stripped_key = stripped_env_key(&key); + // We only care about long option/flag names. + if stripped_key.chars().count() > 1 && matches.opt_defined(&stripped_key) { + match v.into_string() { + Ok(value) => Some((key, value)), + Err(s) => { + eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s); + None } } + } else { + None } - } - - env_var + }, + _ => None }) .collect(); @@ -706,13 +699,33 @@ fn get_setup() -> Setup { exit(0); } + let invalid_error_msg = + |long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| { + error!("Invalid `--{}` / `-{}`: \"{}\"", long, short, invalid); + + if !valid_values.is_empty() { + println!("Valid `--{}` / `-{}` values: {}", long, short, valid_values); + } + + if !default_value.is_empty() { + println!("Default: {}", default_value); + } + }; + + let empty_string_error_msg = |long: &str, short: &str| { + error!("`--{}` / `-{}` can not be an empty string", long, short); + exit(1); + }; + let backend = audio_backend::find(backend_name).unwrap_or_else(|| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", + invalid_error_msg( BACKEND, BACKEND_SHORT, - opt_str(BACKEND).unwrap_or_default() + &opt_str(BACKEND).unwrap_or_default(), + "", + "", ); + list_backends(); exit(1); }); @@ -721,15 +734,15 @@ fn get_setup() -> Setup { .as_deref() .map(|format| { AudioFormat::from_str(format).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - FORMAT, FORMAT_SHORT, format + let default_value = &format!("{:?}", AudioFormat::default()); + invalid_error_msg( + FORMAT, + FORMAT_SHORT, + format, + "F64, F32, S32, S24, S24_3, S16", + default_value, ); - println!( - "Valid `--{}` / `-{}` values: F64, F32, S32, S24, S24_3, S16", - FORMAT, FORMAT_SHORT - ); - println!("Default: {:?}", AudioFormat::default()); + exit(1); }) }) @@ -752,11 +765,7 @@ fn get_setup() -> Setup { backend(device, format); exit(0); } else if value.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - DEVICE, DEVICE_SHORT - ); - exit(1); + empty_string_error_msg(DEVICE, DEVICE_SHORT); } } @@ -785,17 +794,14 @@ fn get_setup() -> Setup { let mixer_type: Option = None; let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", + invalid_error_msg( MIXER_TYPE, MIXER_TYPE_SHORT, - opt_str(MIXER_TYPE).unwrap_or_default() + &opt_str(MIXER_TYPE).unwrap_or_default(), + "alsa, softvol", + "softvol", ); - println!( - "Valid `--{}` / `-{}` values: alsa, softvol", - MIXER_TYPE, MIXER_TYPE_SHORT - ); - println!("Default: softvol"); + exit(1); }); @@ -818,11 +824,14 @@ fn get_setup() -> Setup { let index = opt_str(ALSA_MIXER_INDEX) .map(|index| { index.parse::().unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - ALSA_MIXER_INDEX, ALSA_MIXER_INDEX_SHORT, index + invalid_error_msg( + ALSA_MIXER_INDEX, + ALSA_MIXER_INDEX_SHORT, + &index, + "", + &mixer_default_config.index.to_string(), ); - println!("Default: {}", mixer_default_config.index); + exit(1); }) }) @@ -836,11 +845,7 @@ fn get_setup() -> Setup { #[cfg(feature = "alsa-backend")] if control.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - ALSA_MIXER_CONTROL, ALSA_MIXER_CONTROL_SHORT - ); - exit(1); + empty_string_error_msg(ALSA_MIXER_CONTROL, ALSA_MIXER_CONTROL_SHORT); } #[cfg(not(feature = "alsa-backend"))] @@ -849,24 +854,28 @@ fn get_setup() -> Setup { let volume_range = opt_str(VOLUME_RANGE) .map(|range| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - VOLUME_RANGE, VOLUME_RANGE_SHORT, range - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - VOLUME_RANGE, - VOLUME_RANGE_SHORT, + let valid_values = &format!( + "{} - {}", VALID_VOLUME_RANGE.start(), VALID_VOLUME_RANGE.end() ); + #[cfg(feature = "alsa-backend")] - println!( - "Default: softvol - {}, alsa - what the control supports", + let default_value = &format!( + "softvol - {}, alsa - what the control supports", VolumeCtrl::DEFAULT_DB_RANGE ); + #[cfg(not(feature = "alsa-backend"))] - println!("Default: {}", VolumeCtrl::DEFAULT_DB_RANGE); + let default_value = &VolumeCtrl::DEFAULT_DB_RANGE.to_string(); + + invalid_error_msg( + VOLUME_RANGE, + VOLUME_RANGE_SHORT, + &range, + valid_values, + default_value, + ); }; let range = range.parse::().unwrap_or_else(|_| { @@ -891,15 +900,14 @@ fn get_setup() -> Setup { .as_deref() .map(|volume_ctrl| { VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - VOLUME_CTRL, VOLUME_CTRL_SHORT, volume_ctrl + invalid_error_msg( + VOLUME_CTRL, + VOLUME_CTRL_SHORT, + volume_ctrl, + "cubic, fixed, linear, log", + "log", ); - println!( - "Valid `--{}` / `-{}` values: cubic, fixed, linear, log", - VOLUME_CTRL, VOLUME_CTRL - ); - println!("Default: log"); + exit(1); }) }) @@ -938,10 +946,14 @@ fn get_setup() -> Setup { .map(parse_file_size) .map(|e| { e.unwrap_or_else(|e| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT, e + invalid_error_msg( + CACHE_SIZE_LIMIT, + CACHE_SIZE_LIMIT_SHORT, + &e.to_string(), + "", + "", ); + exit(1); }) }) @@ -970,19 +982,11 @@ fn get_setup() -> Setup { if let Some(username) = opt_str(USERNAME) { if username.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - USERNAME, USERNAME_SHORT - ); - exit(1); + empty_string_error_msg(USERNAME, USERNAME_SHORT); } if let Some(password) = opt_str(PASSWORD) { if password.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - PASSWORD, PASSWORD_SHORT - ); - exit(1); + empty_string_error_msg(PASSWORD, PASSWORD_SHORT); } Some(Credentials::with_password(username, password)) } else { @@ -990,7 +994,6 @@ fn get_setup() -> Setup { Some(creds) if username == creds.username => Some(creds), _ => { let prompt = &format!("Password for {}: ", username); - match rpassword::prompt_password_stderr(prompt) { Ok(password) => { if !password.is_empty() { @@ -1015,6 +1018,9 @@ fn get_setup() -> Setup { } } } else { + if cached_creds.is_some() { + trace!("Using cached credentials."); + } cached_creds } }; @@ -1037,16 +1043,8 @@ fn get_setup() -> Setup { opt_str(ZEROCONF_PORT) .map(|port| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - ZEROCONF_PORT, ZEROCONF_PORT_SHORT, port - ); - println!( - "Valid `--{}` / `-{}` values: 1 - {}", - ZEROCONF_PORT, - ZEROCONF_PORT_SHORT, - u16::MAX - ); + let valid_values = &format!("1 - {}", u16::MAX); + invalid_error_msg(ZEROCONF_PORT, ZEROCONF_PORT_SHORT, &port, valid_values, ""); }; let port = port.parse::().unwrap_or_else(|_| { @@ -1072,36 +1070,37 @@ fn get_setup() -> Setup { let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); if name.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - NAME, NAME_SHORT - ); + empty_string_error_msg(NAME, NAME_SHORT); exit(1); } let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - INITIAL_VOLUME, INITIAL_VOLUME_SHORT, initial_volume - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - INITIAL_VOLUME, - INITIAL_VOLUME_SHORT, + let valid_values = &format!( + "{} - {}", VALID_INITIAL_VOLUME_RANGE.start(), VALID_INITIAL_VOLUME_RANGE.end() ); + #[cfg(feature = "alsa-backend")] - println!( - "Default: {}, or the current value when the alsa mixer is used.", + let default_value = &format!( + "{}, or the current value when the alsa mixer is used.", connect_default_config.initial_volume.unwrap_or_default() ); + #[cfg(not(feature = "alsa-backend"))] - println!( - "Default: {}", - connect_default_config.initial_volume.unwrap_or_default() + let default_value = &connect_default_config + .initial_volume + .unwrap_or_default() + .to_string(); + + invalid_error_msg( + INITIAL_VOLUME, + INITIAL_VOLUME_SHORT, + &initial_volume, + valid_values, + default_value, ); }; @@ -1127,12 +1126,18 @@ fn get_setup() -> Setup { .as_deref() .map(|device_type| { DeviceType::from_str(device_type).unwrap_or_else(|_| { - error!("Invalid `--{}` / `-{}`: \"{}\"", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); - println!("Valid `--{}` / `-{}` values: computer, tablet, smartphone, speaker, tv, avr, stb, audiodongle, \ - gameconsole, castaudio, castvideo, automobile, smartwatch, chromebook, carthing, homething", - DEVICE_TYPE, DEVICE_TYPE_SHORT + invalid_error_msg( + DEVICE_TYPE, + DEVICE_TYPE_SHORT, + device_type, + "computer, tablet, smartphone, \ + speaker, tv, avr, stb, audiodongle, \ + gameconsole, castaudio, castvideo, \ + automobile, smartwatch, chromebook, \ + carthing, homething", + "speaker", ); - println!("Default: speaker"); + exit(1); }) }) @@ -1178,8 +1183,8 @@ fn get_setup() -> Setup { ), ap_port: opt_str(AP_PORT).map(|port| { let on_error = || { - error!("Invalid `--{}` / `-{}`: \"{}\"", AP_PORT, AP_PORT_SHORT, port); - println!("Valid `--{}` / `-{}` values: 1 - {}", AP_PORT, AP_PORT_SHORT, u16::MAX); + let valid_values = &format!("1 - {}", u16::MAX); + invalid_error_msg(AP_PORT, AP_PORT_SHORT, &port, valid_values, ""); }; let port = port.parse::().unwrap_or_else(|_| { @@ -1203,15 +1208,7 @@ fn get_setup() -> Setup { .as_deref() .map(|bitrate| { Bitrate::from_str(bitrate).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - BITRATE, BITRATE_SHORT, bitrate - ); - println!( - "Valid `--{}` / `-{}` values: 96, 160, 320", - BITRATE, BITRATE_SHORT - ); - println!("Default: 160"); + invalid_error_msg(BITRATE, BITRATE_SHORT, bitrate, "96, 160, 320", "160"); exit(1); }) }) @@ -1265,15 +1262,14 @@ fn get_setup() -> Setup { ); let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method + invalid_error_msg( + NORMALISATION_METHOD, + NORMALISATION_METHOD_SHORT, + method, + "basic, dynamic", + &format!("{:?}", player_default_config.normalisation_method), ); - println!( - "Valid `--{}` / `-{}` values: basic, dynamic", - NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT - ); - println!("Default: {:?}", player_default_config.normalisation_method); + exit(1); }); @@ -1292,15 +1288,14 @@ fn get_setup() -> Setup { .as_deref() .map(|gain_type| { NormalisationType::from_str(gain_type).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, gain_type + invalid_error_msg( + NORMALISATION_GAIN_TYPE, + NORMALISATION_GAIN_TYPE_SHORT, + gain_type, + "track, album, auto", + &format!("{:?}", player_default_config.normalisation_type), ); - println!( - "Valid `--{}` / `-{}` values: track, album, auto", - NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, - ); - println!("Default: {:?}", player_default_config.normalisation_type); + exit(1); }) }) @@ -1309,18 +1304,19 @@ fn get_setup() -> Setup { normalisation_pregain = opt_str(NORMALISATION_PREGAIN) .map(|pregain| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_PREGAIN, NORMALISATION_PREGAIN_SHORT, pregain - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_PREGAIN, - NORMALISATION_PREGAIN_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_PREGAIN_RANGE.start(), VALID_NORMALISATION_PREGAIN_RANGE.end() ); - println!("Default: {}", player_default_config.normalisation_pregain); + + invalid_error_msg( + NORMALISATION_PREGAIN, + NORMALISATION_PREGAIN_SHORT, + &pregain, + valid_values, + &player_default_config.normalisation_pregain.to_string(), + ); }; let pregain = pregain.parse::().unwrap_or_else(|_| { @@ -1340,20 +1336,18 @@ fn get_setup() -> Setup { normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) .map(|threshold| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_THRESHOLD, NORMALISATION_THRESHOLD_SHORT, threshold - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_THRESHOLD, - NORMALISATION_THRESHOLD_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_THRESHOLD_RANGE.start(), VALID_NORMALISATION_THRESHOLD_RANGE.end() ); - println!( - "Default: {}", - ratio_to_db(player_default_config.normalisation_threshold) + + invalid_error_msg( + NORMALISATION_THRESHOLD, + NORMALISATION_THRESHOLD_SHORT, + &threshold, + valid_values, + &ratio_to_db(player_default_config.normalisation_threshold).to_string(), ); }; @@ -1374,20 +1368,21 @@ fn get_setup() -> Setup { normalisation_attack = opt_str(NORMALISATION_ATTACK) .map(|attack| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_ATTACK, NORMALISATION_ATTACK_SHORT, attack - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_ATTACK, - NORMALISATION_ATTACK_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_ATTACK_RANGE.start(), VALID_NORMALISATION_ATTACK_RANGE.end() ); - println!( - "Default: {}", - player_default_config.normalisation_attack.as_millis() + + invalid_error_msg( + NORMALISATION_ATTACK, + NORMALISATION_ATTACK_SHORT, + &attack, + valid_values, + &player_default_config + .normalisation_attack + .as_millis() + .to_string(), ); }; @@ -1408,20 +1403,21 @@ fn get_setup() -> Setup { normalisation_release = opt_str(NORMALISATION_RELEASE) .map(|release| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_RELEASE, NORMALISATION_RELEASE_SHORT, release - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_RELEASE, - NORMALISATION_RELEASE_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_RELEASE_RANGE.start(), VALID_NORMALISATION_RELEASE_RANGE.end() ); - println!( - "Default: {}", - player_default_config.normalisation_release.as_millis() + + invalid_error_msg( + NORMALISATION_RELEASE, + NORMALISATION_RELEASE_SHORT, + &release, + valid_values, + &player_default_config + .normalisation_release + .as_millis() + .to_string(), ); }; @@ -1442,18 +1438,19 @@ fn get_setup() -> Setup { normalisation_knee = opt_str(NORMALISATION_KNEE) .map(|knee| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_KNEE, NORMALISATION_KNEE_SHORT, knee - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_KNEE, - NORMALISATION_KNEE_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_KNEE_RANGE.start(), VALID_NORMALISATION_KNEE_RANGE.end() ); - println!("Default: {}", player_default_config.normalisation_knee); + + invalid_error_msg( + NORMALISATION_KNEE, + NORMALISATION_KNEE_SHORT, + &knee, + valid_values, + &player_default_config.normalisation_knee.to_string(), + ); }; let knee = knee.parse::().unwrap_or_else(|_| { @@ -1483,19 +1480,14 @@ fn get_setup() -> Setup { } Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", + invalid_error_msg( DITHER, DITHER_SHORT, - opt_str(DITHER).unwrap_or_default() - ); - println!( - "Valid `--{}` / `-{}` values: none, gpdf, tpdf, tpdf_hp", - DITHER, DITHER_SHORT - ); - println!( - "Default: tpdf for formats S16, S24, S24_3 and none for other formats" + &opt_str(DITHER).unwrap_or_default(), + "none, gpdf, tpdf, tpdf_hp", + "tpdf for formats S16, S24, S24_3 and none for other formats", ); + exit(1); })) } From 368bee10885d5d0e528e45328676e59857cd6896 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Mon, 13 Dec 2021 16:40:26 -0600 Subject: [PATCH 74/95] condense some option parsings --- src/main.rs | 229 ++++++++++++++++++---------------------------------- 1 file changed, 78 insertions(+), 151 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7e13a323..2ce526e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -852,8 +852,9 @@ fn get_setup() -> Setup { let control = mixer_default_config.control; let volume_range = opt_str(VOLUME_RANGE) - .map(|range| { - let on_error = || { + .map(|range| match range.parse::() { + Ok(value) if (VALID_VOLUME_RANGE).contains(&value) => value, + _ => { let valid_values = &format!( "{} - {}", VALID_VOLUME_RANGE.start(), @@ -876,19 +877,9 @@ fn get_setup() -> Setup { valid_values, default_value, ); - }; - let range = range.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_VOLUME_RANGE).contains(&range) { - on_error(); exit(1); } - - range }) .unwrap_or_else(|| match mixer_type.as_deref() { #[cfg(feature = "alsa-backend")] @@ -1041,23 +1032,14 @@ fn get_setup() -> Setup { let zeroconf_port = if enable_discovery { opt_str(ZEROCONF_PORT) - .map(|port| { - let on_error = || { + .map(|port| match port.parse::() { + Ok(value) if value != 0 => value, + _ => { let valid_values = &format!("1 - {}", u16::MAX); invalid_error_msg(ZEROCONF_PORT, ZEROCONF_PORT_SHORT, &port, valid_values, ""); - }; - let port = port.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if port == 0 { - on_error(); exit(1); } - - port }) .unwrap_or(0) } else { @@ -1076,44 +1058,39 @@ fn get_setup() -> Setup { let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { - let on_error = || { - let valid_values = &format!( - "{} - {}", - VALID_INITIAL_VOLUME_RANGE.start(), - VALID_INITIAL_VOLUME_RANGE.end() - ); + let volume = match initial_volume.parse::() { + Ok(value) if (VALID_INITIAL_VOLUME_RANGE).contains(&value) => value, + _ => { + let valid_values = &format!( + "{} - {}", + VALID_INITIAL_VOLUME_RANGE.start(), + VALID_INITIAL_VOLUME_RANGE.end() + ); - #[cfg(feature = "alsa-backend")] - let default_value = &format!( - "{}, or the current value when the alsa mixer is used.", - connect_default_config.initial_volume.unwrap_or_default() - ); + #[cfg(feature = "alsa-backend")] + let default_value = &format!( + "{}, or the current value when the alsa mixer is used.", + connect_default_config.initial_volume.unwrap_or_default() + ); - #[cfg(not(feature = "alsa-backend"))] - let default_value = &connect_default_config - .initial_volume - .unwrap_or_default() - .to_string(); + #[cfg(not(feature = "alsa-backend"))] + let default_value = &connect_default_config + .initial_volume + .unwrap_or_default() + .to_string(); - invalid_error_msg( - INITIAL_VOLUME, - INITIAL_VOLUME_SHORT, - &initial_volume, - valid_values, - default_value, - ); + invalid_error_msg( + INITIAL_VOLUME, + INITIAL_VOLUME_SHORT, + &initial_volume, + valid_values, + default_value, + ); + + exit(1); + } }; - let volume = initial_volume.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_INITIAL_VOLUME_RANGE).contains(&volume) { - on_error(); - exit(1); - } - (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 }) .or_else(|| match mixer_type.as_deref() { @@ -1135,7 +1112,7 @@ fn get_setup() -> Setup { gameconsole, castaudio, castvideo, \ automobile, smartwatch, chromebook, \ carthing, homething", - "speaker", + DeviceType::default().into(), ); exit(1); @@ -1181,23 +1158,14 @@ fn get_setup() -> Setup { } }, ), - ap_port: opt_str(AP_PORT).map(|port| { - let on_error = || { + ap_port: opt_str(AP_PORT).map(|port| match port.parse::() { + Ok(value) if value != 0 => value, + _ => { let valid_values = &format!("1 - {}", u16::MAX); invalid_error_msg(AP_PORT, AP_PORT_SHORT, &port, valid_values, ""); - }; - let port = port.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if port == 0 { - on_error(); exit(1); } - - port }), }; @@ -1302,8 +1270,9 @@ fn get_setup() -> Setup { .unwrap_or(player_default_config.normalisation_type); normalisation_pregain = opt_str(NORMALISATION_PREGAIN) - .map(|pregain| { - let on_error = || { + .map(|pregain| match pregain.parse::() { + Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value, + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_PREGAIN_RANGE.start(), @@ -1317,25 +1286,18 @@ fn get_setup() -> Setup { valid_values, &player_default_config.normalisation_pregain.to_string(), ); - }; - let pregain = pregain.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_PREGAIN_RANGE).contains(&pregain) { - on_error(); exit(1); } - - pregain }) .unwrap_or(player_default_config.normalisation_pregain); normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) - .map(|threshold| { - let on_error = || { + .map(|threshold| match threshold.parse::() { + Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => { + db_to_ratio(value) + } + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_THRESHOLD_RANGE.start(), @@ -1349,25 +1311,18 @@ fn get_setup() -> Setup { valid_values, &ratio_to_db(player_default_config.normalisation_threshold).to_string(), ); - }; - let threshold = threshold.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_THRESHOLD_RANGE).contains(&threshold) { - on_error(); exit(1); } - - db_to_ratio(threshold) }) .unwrap_or(player_default_config.normalisation_threshold); normalisation_attack = opt_str(NORMALISATION_ATTACK) - .map(|attack| { - let on_error = || { + .map(|attack| match attack.parse::() { + Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => { + Duration::from_millis(value) + } + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_ATTACK_RANGE.start(), @@ -1384,25 +1339,18 @@ fn get_setup() -> Setup { .as_millis() .to_string(), ); - }; - let attack = attack.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_ATTACK_RANGE).contains(&attack) { - on_error(); exit(1); } - - Duration::from_millis(attack) }) .unwrap_or(player_default_config.normalisation_attack); normalisation_release = opt_str(NORMALISATION_RELEASE) - .map(|release| { - let on_error = || { + .map(|release| match release.parse::() { + Ok(value) if (VALID_NORMALISATION_RELEASE_RANGE).contains(&value) => { + Duration::from_millis(value) + } + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_RELEASE_RANGE.start(), @@ -1419,25 +1367,16 @@ fn get_setup() -> Setup { .as_millis() .to_string(), ); - }; - let release = release.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_RELEASE_RANGE).contains(&release) { - on_error(); exit(1); } - - Duration::from_millis(release) }) .unwrap_or(player_default_config.normalisation_release); normalisation_knee = opt_str(NORMALISATION_KNEE) - .map(|knee| { - let on_error = || { + .map(|knee| match knee.parse::() { + Ok(value) if (VALID_NORMALISATION_KNEE_RANGE).contains(&value) => value, + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_KNEE_RANGE.start(), @@ -1451,47 +1390,35 @@ fn get_setup() -> Setup { valid_values, &player_default_config.normalisation_knee.to_string(), ); - }; - let knee = knee.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_KNEE_RANGE).contains(&knee) { - on_error(); exit(1); } - - knee }) .unwrap_or(player_default_config.normalisation_knee); } let ditherer_name = opt_str(DITHER); let ditherer = match ditherer_name.as_deref() { - // explicitly disabled on command line - Some("none") => None, - // explicitly set on command line - Some(_) => { - if matches!(format, AudioFormat::F64 | AudioFormat::F32) { - error!("Dithering is not available with format: {:?}.", format); - exit(1); - } + Some(value) => match value { + "none" => None, + _ => match format { + AudioFormat::F64 | AudioFormat::F32 => { + error!("Dithering is not available with format: {:?}.", format); + exit(1); + } + _ => Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { + invalid_error_msg( + DITHER, + DITHER_SHORT, + &opt_str(DITHER).unwrap_or_default(), + "none, gpdf, tpdf, tpdf_hp for formats S16, S24, S24_3, S32, none for formats F32, F64", + "tpdf for formats S16, S24, S24_3 and none for formats S32, F32, F64", + ); - Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { - invalid_error_msg( - DITHER, - DITHER_SHORT, - &opt_str(DITHER).unwrap_or_default(), - "none, gpdf, tpdf, tpdf_hp", - "tpdf for formats S16, S24, S24_3 and none for other formats", - ); - - exit(1); - })) - } - // nothing set on command line => use default + exit(1); + })), + }, + }, None => match format { AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { player_default_config.ditherer From 67836b5b02f2990376849fc04d2f9cd0316f1e0b Mon Sep 17 00:00:00 2001 From: Mateusz Mojsiejuk Date: Tue, 14 Dec 2021 21:23:13 +0100 Subject: [PATCH 75/95] Added arm64 target to docker run examples. Also removed feature quotes as they're not nessesary and don't match the non-quoted examples of targets in the WIKI --- contrib/Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 74b83d31..aa29183c 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -9,8 +9,10 @@ # # If only one architecture is desired, cargo can be invoked directly with the appropriate options : # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend + # $ docker run -v /tmp/librespot-build:/build librespot-cross contrib/docker-build-pi-armv6hf.sh FROM debian:stretch From d5efb8a620554ced4fda731c72046c4b1af53111 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 14 Dec 2021 16:49:09 -0600 Subject: [PATCH 76/95] Dynamic failable buffer sizing alsa-backend Dynamically set the alsa buffer and period based on the device's reported min/max buffer and period sizes. In the event of failure use the device's defaults. This should have no effect on devices that allow for reasonable buffer and period sizes but would allow us to be more forgiving with less reasonable devices or configurations. Closes: https://github.com/librespot-org/librespot/issues/895 --- playback/src/audio_backend/alsa.rs | 187 +++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 13 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index e572f953..4f82a097 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -4,16 +4,18 @@ use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use alsa::device_name::HintIter; -use alsa::pcm::{Access, Format, HwParams, PCM}; +use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, ValueOr}; use std::cmp::min; use std::process::exit; -use std::time::Duration; use thiserror::Error; -// 0.5 sec buffer. -const PERIOD_TIME: Duration = Duration::from_millis(100); -const BUFFER_TIME: Duration = Duration::from_millis(500); +const MAX_BUFFER: Frames = (SAMPLE_RATE / 2) as Frames; +const MIN_BUFFER: Frames = (SAMPLE_RATE / 10) as Frames; +const ZERO_FRAMES: Frames = 0; + +const MAX_PERIOD_DIVISOR: Frames = 4; +const MIN_PERIOD_DIVISOR: Frames = 10; #[derive(Debug, Error)] enum AlsaError { @@ -195,28 +197,187 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> e, })?; - hwp.set_buffer_time_near(BUFFER_TIME.as_micros() as u32, ValueOr::Nearest) - .map_err(AlsaError::HwParams)?; + // Clone the hwp while it's in + // a good working state so that + // in the event of an error setting + // the buffer and period sizes + // we can use the good working clone + // instead of the hwp that's in an + // error state. + let hwp_clone = hwp.clone(); - hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest) - .map_err(AlsaError::HwParams)?; + // At a sampling rate of 44100: + // The largest buffer is 22050 Frames (500ms) with 5512 Frame periods (125ms). + // The smallest buffer is 4410 Frames (100ms) with 441 Frame periods (10ms). + // Actual values may vary. + // + // Larger buffer and period sizes are preferred as extremely small values + // will cause high CPU useage. + // + // If no buffer or period size is in those ranges or an error happens + // trying to set the buffer or period size use the device's defaults + // which may not be ideal but are *hopefully* serviceable. - pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; + let buffer_size = { + let max = match hwp.get_buffer_size_max() { + Err(e) => { + trace!("Error getting the device's max Buffer size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; - let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; + let min = match hwp.get_buffer_size_min() { + Err(e) => { + trace!("Error getting the device's min Buffer size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; + + let buffer_size = if min < max { + match (MIN_BUFFER..=MAX_BUFFER) + .rev() + .find(|f| (min..=max).contains(f)) + { + Some(size) => { + trace!("Desired Frames per Buffer: {:?}", size); + + match hwp.set_buffer_size_near(size) { + Err(e) => { + trace!("Error setting the device's Buffer size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + } + } + None => { + trace!("No Desired Buffer size in range reported by the device."); + ZERO_FRAMES + } + } + } else { + trace!("The device's min reported Buffer size was greater than or equal to it's max reported Buffer size."); + ZERO_FRAMES + }; + + if buffer_size == ZERO_FRAMES { + trace!( + "Desired Buffer Frame range: {:?} - {:?}", + MIN_BUFFER, + MAX_BUFFER + ); + + trace!( + "Actual Buffer Frame range as reported by the device: {:?} - {:?}", + min, + max + ); + } + + buffer_size + }; + + let period_size = { + if buffer_size == ZERO_FRAMES { + ZERO_FRAMES + } else { + let max = match hwp.get_period_size_max() { + Err(e) => { + trace!("Error getting the device's max Period size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; + + let min = match hwp.get_period_size_min() { + Err(e) => { + trace!("Error getting the device's min Period size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; + + let max_period = buffer_size / MAX_PERIOD_DIVISOR; + let min_period = buffer_size / MIN_PERIOD_DIVISOR; + + let period_size = if min < max && min_period < max_period { + match (min_period..=max_period) + .rev() + .find(|f| (min..=max).contains(f)) + { + Some(size) => { + trace!("Desired Frames per Period: {:?}", size); + + match hwp.set_period_size_near(size, ValueOr::Nearest) { + Err(e) => { + trace!("Error setting the device's Period size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + } + } + None => { + trace!("No Desired Period size in range reported by the device."); + ZERO_FRAMES + } + } + } else { + trace!("The device's min reported Period size was greater than or equal to it's max reported Period size,"); + trace!("or the desired min Period size was greater than or equal to the desired max Period size."); + ZERO_FRAMES + }; + + if period_size == ZERO_FRAMES { + trace!("Buffer size: {:?}", buffer_size); + + trace!( + "Desired Period Frame range: {:?} (Buffer size / {:?}) - {:?} (Buffer size / {:?})", + min_period, + MIN_PERIOD_DIVISOR, + max_period, + MAX_PERIOD_DIVISOR, + ); + + trace!( + "Actual Period Frame range as reported by the device: {:?} - {:?}", + min, + max + ); + } + + period_size + } + }; + + if buffer_size == ZERO_FRAMES || period_size == ZERO_FRAMES { + trace!( + "Failed to set Buffer and/or Period size, falling back to the device's defaults." + ); + + trace!("You may experience higher than normal CPU usage and/or audio issues."); + + pcm.hw_params(&hwp_clone).map_err(AlsaError::Pcm)?; + } else { + pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; + } + + let hwp = pcm.hw_params_current().map_err(AlsaError::Pcm)?; // Don't assume we got what we wanted. Ask to make sure. let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?; let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?; + let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; + swp.set_start_threshold(frames_per_buffer - frames_per_period) .map_err(AlsaError::SwParams)?; pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; - trace!("Frames per Buffer: {:?}", frames_per_buffer); - trace!("Frames per Period: {:?}", frames_per_period); + trace!("Actual Frames per Buffer: {:?}", frames_per_buffer); + trace!("Actual Frames per Period: {:?}", frames_per_period); // Let ALSA do the math for us. pcm.frames_to_bytes(frames_per_period) as usize From 2f7b9863d9c2862cf16890a755268bef3f3e50bb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 16 Dec 2021 22:42:37 +0100 Subject: [PATCH 77/95] Implement CDN for audio files --- Cargo.lock | 68 ++++++++---- audio/Cargo.toml | 6 +- audio/src/fetch/mod.rs | 165 ++++++++++++++++------------ audio/src/fetch/receive.rs | 169 +++++++++++++---------------- core/Cargo.toml | 5 +- core/src/apresolve.rs | 4 +- core/src/cdn_url.rs | 151 ++++++++++++++++++++++++++ {metadata => core}/src/date.rs | 16 ++- core/src/http_client.rs | 88 ++++++++++----- core/src/lib.rs | 2 + core/src/spclient.rs | 66 ++++++++--- metadata/src/album.rs | 2 +- metadata/src/availability.rs | 3 +- metadata/src/episode.rs | 2 +- metadata/src/error.rs | 3 +- metadata/src/lib.rs | 1 - metadata/src/playlist/attribute.rs | 3 +- metadata/src/playlist/item.rs | 3 +- metadata/src/playlist/list.rs | 2 +- metadata/src/sale_period.rs | 3 +- metadata/src/track.rs | 2 +- playback/src/player.rs | 4 +- protocol/build.rs | 1 + 23 files changed, 518 insertions(+), 251 deletions(-) create mode 100644 core/src/cdn_url.rs rename {metadata => core}/src/date.rs (85%) diff --git a/Cargo.lock b/Cargo.lock index 5aa66853..3e28c806 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -447,9 +447,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e" +checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" dependencies = [ "futures-channel", "futures-core", @@ -462,9 +462,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27" +checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" dependencies = [ "futures-core", "futures-sink", @@ -472,15 +472,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" [[package]] name = "futures-executor" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" dependencies = [ "futures-core", "futures-task", @@ -489,16 +489,18 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" [[package]] name = "futures-macro" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd" +checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" dependencies = [ + "autocfg", + "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -506,22 +508,23 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af" +checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" [[package]] name = "futures-task" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" [[package]] name = "futures-util" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" dependencies = [ + "autocfg", "futures-channel", "futures-core", "futures-io", @@ -531,6 +534,8 @@ dependencies = [ "memchr", "pin-project-lite", "pin-utils", + "proc-macro-hack", + "proc-macro-nested", "slab", ] @@ -726,9 +731,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" +checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd" dependencies = [ "bytes", "fnv", @@ -861,9 +866,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.15" +version = "0.14.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" +checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" dependencies = [ "bytes", "futures-channel", @@ -1215,10 +1220,14 @@ dependencies = [ "aes-ctr", "byteorder", "bytes", + "futures-core", + "futures-executor", "futures-util", + "hyper", "librespot-core", "log", "tempfile", + "thiserror", "tokio", ] @@ -1249,6 +1258,7 @@ dependencies = [ "base64", "byteorder", "bytes", + "chrono", "env_logger", "form_urlencoded", "futures-core", @@ -1272,6 +1282,8 @@ dependencies = [ "protobuf", "quick-xml", "rand", + "rustls", + "rustls-native-certs", "serde", "serde_json", "sha-1", @@ -1917,6 +1929,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + [[package]] name = "proc-macro2" version = "1.0.33" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 77855e62..d5a7a074 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -14,7 +14,11 @@ version = "0.3.1" aes-ctr = "0.6" byteorder = "1.4" bytes = "1.0" -log = "0.4" +futures-core = { version = "0.3", default-features = false } +futures-executor = "0.3" futures-util = { version = "0.3", default_features = false } +hyper = { version = "0.14", features = ["client"] } +log = "0.4" tempfile = "3.1" +thiserror = "1.0" tokio = { version = "1", features = ["sync", "macros"] } diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index b68f6858..97037d6e 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -7,36 +7,55 @@ use std::sync::atomic::{self, AtomicUsize}; use std::sync::{Arc, Condvar, Mutex}; use std::time::{Duration, Instant}; -use byteorder::{BigEndian, ByteOrder}; -use futures_util::{future, StreamExt, TryFutureExt, TryStreamExt}; +use futures_util::future::IntoStream; +use futures_util::{StreamExt, TryFutureExt}; +use hyper::client::ResponseFuture; +use hyper::header::CONTENT_RANGE; +use hyper::Body; use tempfile::NamedTempFile; +use thiserror::Error; use tokio::sync::{mpsc, oneshot}; -use librespot_core::channel::{ChannelData, ChannelError, ChannelHeaders}; +use librespot_core::cdn_url::{CdnUrl, CdnUrlError}; use librespot_core::file_id::FileId; use librespot_core::session::Session; +use librespot_core::spclient::SpClientError; -use self::receive::{audio_file_fetch, request_range}; +use self::receive::audio_file_fetch; use crate::range_set::{Range, RangeSet}; +#[derive(Error, Debug)] +pub enum AudioFileError { + #[error("could not complete CDN request: {0}")] + Cdn(hyper::Error), + #[error("empty response")] + Empty, + #[error("error parsing response")] + Parsing, + #[error("could not complete API request: {0}")] + SpClient(#[from] SpClientError), + #[error("could not get CDN URL: {0}")] + Url(#[from] CdnUrlError), +} + /// The minimum size of a block that is requested from the Spotify servers in one request. /// This is the block size that is typically requested while doing a `seek()` on a file. /// Note: smaller requests can happen if part of the block is downloaded already. -const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 16; +pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 256; /// The amount of data that is requested when initially opening a file. /// Note: if the file is opened to play from the beginning, the amount of data to /// read ahead is requested in addition to this amount. If the file is opened to seek to /// another position, then only this amount is requested on the first request. -const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 16; +pub const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 128; /// The ping time that is used for calculations before a ping time was actually measured. -const INITIAL_PING_TIME_ESTIMATE: Duration = Duration::from_millis(500); +pub const INITIAL_PING_TIME_ESTIMATE: Duration = Duration::from_millis(500); /// If the measured ping time to the Spotify server is larger than this value, it is capped /// to avoid run-away block sizes and pre-fetching. -const MAXIMUM_ASSUMED_PING_TIME: Duration = Duration::from_millis(1500); +pub const MAXIMUM_ASSUMED_PING_TIME: Duration = Duration::from_millis(1500); /// Before playback starts, this many seconds of data must be present. /// Note: the calculations are done using the nominal bitrate of the file. The actual amount @@ -65,7 +84,7 @@ pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f32 = 10.0; /// If the amount of data that is pending (requested but not received) is less than a certain amount, /// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more /// data is calculated as ` < PREFETCH_THRESHOLD_FACTOR * * ` -const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0; +pub const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0; /// Similar to `PREFETCH_THRESHOLD_FACTOR`, but it also takes the current download rate into account. /// The formula used is ` < FAST_PREFETCH_THRESHOLD_FACTOR * * ` @@ -74,16 +93,16 @@ const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0; /// the download rate ramps up. However, this comes at the cost that it might hurt ping time if a seek is /// performed while downloading. Values smaller than `1.0` cause the download rate to collapse and effectively /// only `PREFETCH_THRESHOLD_FACTOR` is in effect. Thus, set to `0.0` if bandwidth saturation is not wanted. -const FAST_PREFETCH_THRESHOLD_FACTOR: f32 = 1.5; +pub const FAST_PREFETCH_THRESHOLD_FACTOR: f32 = 1.5; /// Limit the number of requests that are pending simultaneously before pre-fetching data. Pending -/// requests share bandwidth. Thus, havint too many requests can lead to the one that is needed next +/// requests share bandwidth. Thus, having too many requests can lead to the one that is needed next /// for playback to be delayed leading to a buffer underrun. This limit has the effect that a new /// pre-fetch request is only sent if less than `MAX_PREFETCH_REQUESTS` are pending. -const MAX_PREFETCH_REQUESTS: usize = 4; +pub const MAX_PREFETCH_REQUESTS: usize = 4; /// The time we will wait to obtain status updates on downloading. -const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(1); +pub const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(1); pub enum AudioFile { Cached(fs::File), @@ -91,7 +110,16 @@ pub enum AudioFile { } #[derive(Debug)] -enum StreamLoaderCommand { +pub struct StreamingRequest { + streamer: IntoStream, + initial_body: Option, + offset: usize, + length: usize, + request_time: Instant, +} + +#[derive(Debug)] +pub enum StreamLoaderCommand { Fetch(Range), // signal the stream loader to fetch a range of the file RandomAccessMode(), // optimise download strategy for random access StreamMode(), // optimise download strategy for streaming @@ -244,9 +272,9 @@ enum DownloadStrategy { } struct AudioFileShared { - file_id: FileId, + cdn_url: CdnUrl, file_size: usize, - stream_data_rate: usize, + bytes_per_second: usize, cond: Condvar, download_status: Mutex, download_strategy: Mutex, @@ -255,19 +283,13 @@ struct AudioFileShared { read_position: AtomicUsize, } -pub struct InitialData { - rx: ChannelData, - length: usize, - request_sent_time: Instant, -} - impl AudioFile { pub async fn open( session: &Session, file_id: FileId, bytes_per_second: usize, play_from_beginning: bool, - ) -> Result { + ) -> Result { if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) { debug!("File {} already in cache", file_id); return Ok(AudioFile::Cached(file)); @@ -276,35 +298,13 @@ impl AudioFile { debug!("Downloading file {}", file_id); let (complete_tx, complete_rx) = oneshot::channel(); - let mut length = if play_from_beginning { - INITIAL_DOWNLOAD_SIZE - + max( - (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, - (INITIAL_PING_TIME_ESTIMATE.as_secs_f32() - * READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS - * bytes_per_second as f32) as usize, - ) - } else { - INITIAL_DOWNLOAD_SIZE - }; - if length % 4 != 0 { - length += 4 - (length % 4); - } - let (headers, rx) = request_range(session, file_id, 0, length).split(); - - let initial_data = InitialData { - rx, - length, - request_sent_time: Instant::now(), - }; let streaming = AudioFileStreaming::open( session.clone(), - initial_data, - headers, file_id, complete_tx, bytes_per_second, + play_from_beginning, ); let session_ = session.clone(); @@ -343,24 +343,58 @@ impl AudioFile { impl AudioFileStreaming { pub async fn open( session: Session, - initial_data: InitialData, - headers: ChannelHeaders, file_id: FileId, complete_tx: oneshot::Sender, - streaming_data_rate: usize, - ) -> Result { - let (_, data) = headers - .try_filter(|(id, _)| future::ready(*id == 0x3)) - .next() - .await - .unwrap()?; + bytes_per_second: usize, + play_from_beginning: bool, + ) -> Result { + let download_size = if play_from_beginning { + INITIAL_DOWNLOAD_SIZE + + max( + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, + (INITIAL_PING_TIME_ESTIMATE.as_secs_f32() + * READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS + * bytes_per_second as f32) as usize, + ) + } else { + INITIAL_DOWNLOAD_SIZE + }; - let size = BigEndian::read_u32(&data) as usize * 4; + let mut cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?; + let url = cdn_url.get_url()?; + + let mut streamer = session.spclient().stream_file(url, 0, download_size)?; + let request_time = Instant::now(); + + // Get the first chunk with the headers to get the file size. + // The remainder of that chunk with possibly also a response body is then + // further processed in `audio_file_fetch`. + let response = match streamer.next().await { + Some(Ok(data)) => data, + Some(Err(e)) => return Err(AudioFileError::Cdn(e)), + None => return Err(AudioFileError::Empty), + }; + let header_value = response + .headers() + .get(CONTENT_RANGE) + .ok_or(AudioFileError::Parsing)?; + + let str_value = header_value.to_str().map_err(|_| AudioFileError::Parsing)?; + let file_size_str = str_value.split('/').last().ok_or(AudioFileError::Parsing)?; + let file_size = file_size_str.parse().map_err(|_| AudioFileError::Parsing)?; + + let initial_request = StreamingRequest { + streamer, + initial_body: Some(response.into_body()), + offset: 0, + length: download_size, + request_time, + }; let shared = Arc::new(AudioFileShared { - file_id, - file_size: size, - stream_data_rate: streaming_data_rate, + cdn_url, + file_size, + bytes_per_second, cond: Condvar::new(), download_status: Mutex::new(AudioFileDownloadStatus { requested: RangeSet::new(), @@ -372,20 +406,17 @@ impl AudioFileStreaming { read_position: AtomicUsize::new(0), }); - let mut write_file = NamedTempFile::new().unwrap(); - write_file.as_file().set_len(size as u64).unwrap(); - write_file.seek(SeekFrom::Start(0)).unwrap(); - + // TODO : use new_in() to store securely in librespot directory + let write_file = NamedTempFile::new().unwrap(); let read_file = write_file.reopen().unwrap(); - // let (seek_tx, seek_rx) = mpsc::unbounded(); let (stream_loader_command_tx, stream_loader_command_rx) = mpsc::unbounded_channel::(); session.spawn(audio_file_fetch( session.clone(), shared.clone(), - initial_data, + initial_request, write_file, stream_loader_command_rx, complete_tx, @@ -422,10 +453,10 @@ impl Read for AudioFileStreaming { let length_to_request = length + max( (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() - * self.shared.stream_data_rate as f32) as usize, + * self.shared.bytes_per_second as f32) as usize, (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS * ping_time_seconds - * self.shared.stream_data_rate as f32) as usize, + * self.shared.bytes_per_second as f32) as usize, ); min(length_to_request, self.shared.file_size - offset) } diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 7b797b02..6157040f 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -4,56 +4,21 @@ use std::sync::{atomic, Arc}; use std::time::{Duration, Instant}; use atomic::Ordering; -use byteorder::{BigEndian, WriteBytesExt}; use bytes::Bytes; use futures_util::StreamExt; use tempfile::NamedTempFile; use tokio::sync::{mpsc, oneshot}; -use librespot_core::channel::{Channel, ChannelData}; -use librespot_core::file_id::FileId; -use librespot_core::packet::PacketType; use librespot_core::session::Session; use crate::range_set::{Range, RangeSet}; -use super::{AudioFileShared, DownloadStrategy, InitialData, StreamLoaderCommand}; +use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand, StreamingRequest}; use super::{ FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, }; -pub fn request_range(session: &Session, file: FileId, offset: usize, length: usize) -> Channel { - assert!( - offset % 4 == 0, - "Range request start positions must be aligned by 4 bytes." - ); - assert!( - length % 4 == 0, - "Range request range lengths must be aligned by 4 bytes." - ); - let start = offset / 4; - let end = (offset + length) / 4; - - let (id, channel) = session.channel().allocate(); - - let mut data: Vec = Vec::new(); - data.write_u16::(id).unwrap(); - data.write_u8(0).unwrap(); - data.write_u8(1).unwrap(); - data.write_u16::(0x0000).unwrap(); - data.write_u32::(0x00000000).unwrap(); - data.write_u32::(0x00009C40).unwrap(); - data.write_u32::(0x00020000).unwrap(); - data.write_all(&file.0).unwrap(); - data.write_u32::(start as u32).unwrap(); - data.write_u32::(end as u32).unwrap(); - - session.send_packet(PacketType::StreamChunk, data); - - channel -} - struct PartialFileData { offset: usize, data: Bytes, @@ -67,13 +32,13 @@ enum ReceivedData { async fn receive_data( shared: Arc, file_data_tx: mpsc::UnboundedSender, - mut data_rx: ChannelData, - initial_data_offset: usize, - initial_request_length: usize, - request_sent_time: Instant, + mut request: StreamingRequest, ) { - let mut data_offset = initial_data_offset; - let mut request_length = initial_request_length; + let requested_offset = request.offset; + let requested_length = request.length; + + let mut data_offset = requested_offset; + let mut request_length = requested_length; let old_number_of_request = shared .number_of_open_requests @@ -82,21 +47,31 @@ async fn receive_data( let mut measure_ping_time = old_number_of_request == 0; let result = loop { - let data = match data_rx.next().await { - Some(Ok(data)) => data, - Some(Err(e)) => break Err(e), - None => break Ok(()), + let body = match request.initial_body.take() { + Some(data) => data, + None => match request.streamer.next().await { + Some(Ok(response)) => response.into_body(), + Some(Err(e)) => break Err(e), + None => break Ok(()), + }, + }; + + let data = match hyper::body::to_bytes(body).await { + Ok(bytes) => bytes, + Err(e) => break Err(e), }; if measure_ping_time { - let mut duration = Instant::now() - request_sent_time; + let mut duration = Instant::now() - request.request_time; if duration > MAXIMUM_ASSUMED_PING_TIME { duration = MAXIMUM_ASSUMED_PING_TIME; } let _ = file_data_tx.send(ReceivedData::ResponseTime(duration)); measure_ping_time = false; } + let data_size = data.len(); + let _ = file_data_tx.send(ReceivedData::Data(PartialFileData { offset: data_offset, data, @@ -104,8 +79,8 @@ async fn receive_data( data_offset += data_size; if request_length < data_size { warn!( - "Data receiver for range {} (+{}) received more data from server than requested.", - initial_data_offset, initial_request_length + "Data receiver for range {} (+{}) received more data from server than requested ({} instead of {}).", + requested_offset, requested_length, data_size, request_length ); request_length = 0; } else { @@ -117,6 +92,8 @@ async fn receive_data( } }; + drop(request.streamer); + if request_length > 0 { let missing_range = Range::new(data_offset, request_length); @@ -129,15 +106,15 @@ async fn receive_data( .number_of_open_requests .fetch_sub(1, Ordering::SeqCst); - if result.is_err() { - warn!( - "Error from channel for data receiver for range {} (+{}).", - initial_data_offset, initial_request_length + if let Err(e) = result { + error!( + "Error from streamer for range {} (+{}): {:?}", + requested_offset, requested_length, e ); } else if request_length > 0 { warn!( - "Data receiver for range {} (+{}) received less data from server than requested.", - initial_data_offset, initial_request_length + "Streamer for range {} (+{}) received less data from server than requested.", + requested_offset, requested_length ); } } @@ -164,12 +141,12 @@ impl AudioFileFetch { *(self.shared.download_strategy.lock().unwrap()) } - fn download_range(&mut self, mut offset: usize, mut length: usize) { + fn download_range(&mut self, offset: usize, mut length: usize) { if length < MINIMUM_DOWNLOAD_SIZE { length = MINIMUM_DOWNLOAD_SIZE; } - // ensure the values are within the bounds and align them by 4 for the spotify protocol. + // ensure the values are within the bounds if offset >= self.shared.file_size { return; } @@ -182,15 +159,6 @@ impl AudioFileFetch { length = self.shared.file_size - offset; } - if offset % 4 != 0 { - length += offset % 4; - offset -= offset % 4; - } - - if length % 4 != 0 { - length += 4 - (length % 4); - } - let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length)); @@ -199,25 +167,43 @@ impl AudioFileFetch { ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); + let cdn_url = &self.shared.cdn_url; + let file_id = cdn_url.file_id; + for range in ranges_to_request.iter() { - let (_headers, data) = request_range( - &self.session, - self.shared.file_id, - range.start, - range.length, - ) - .split(); + match cdn_url.urls.first() { + Some(url) => { + match self + .session + .spclient() + .stream_file(&url.0, range.start, range.length) + { + Ok(streamer) => { + download_status.requested.add_range(range); - download_status.requested.add_range(range); + let streaming_request = StreamingRequest { + streamer, + initial_body: None, + offset: range.start, + length: range.length, + request_time: Instant::now(), + }; - self.session.spawn(receive_data( - self.shared.clone(), - self.file_data_tx.clone(), - data, - range.start, - range.length, - Instant::now(), - )); + self.session.spawn(receive_data( + self.shared.clone(), + self.file_data_tx.clone(), + streaming_request, + )); + } + Err(e) => { + error!("Unable to open stream for track <{}>: {:?}", file_id, e); + } + } + } + None => { + error!("Unable to get CDN URL for track <{}>", file_id); + } + } } } @@ -268,8 +254,7 @@ impl AudioFileFetch { fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow { match data { ReceivedData::ResponseTime(response_time) => { - // chatty - // trace!("Ping time estimated as: {}ms", response_time.as_millis()); + trace!("Ping time estimated as: {} ms", response_time.as_millis()); // prune old response times. Keep at most two so we can push a third. while self.network_response_times.len() >= 3 { @@ -356,7 +341,7 @@ impl AudioFileFetch { pub(super) async fn audio_file_fetch( session: Session, shared: Arc, - initial_data: InitialData, + initial_request: StreamingRequest, output: NamedTempFile, mut stream_loader_command_rx: mpsc::UnboundedReceiver, complete_tx: oneshot::Sender, @@ -364,7 +349,10 @@ pub(super) async fn audio_file_fetch( let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel(); { - let requested_range = Range::new(0, initial_data.length); + let requested_range = Range::new( + initial_request.offset, + initial_request.offset + initial_request.length, + ); let mut download_status = shared.download_status.lock().unwrap(); download_status.requested.add_range(&requested_range); } @@ -372,14 +360,11 @@ pub(super) async fn audio_file_fetch( session.spawn(receive_data( shared.clone(), file_data_tx.clone(), - initial_data.rx, - 0, - initial_data.length, - initial_data.request_sent_time, + initial_request, )); let mut fetch = AudioFileFetch { - session, + session: session.clone(), shared, output: Some(output), @@ -424,7 +409,7 @@ pub(super) async fn audio_file_fetch( let desired_pending_bytes = max( (PREFETCH_THRESHOLD_FACTOR * ping_time_seconds - * fetch.shared.stream_data_rate as f32) as usize, + * fetch.shared.bytes_per_second as f32) as usize, (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f32) as usize, ); diff --git a/core/Cargo.toml b/core/Cargo.toml index 54fc1de7..876a0038 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,6 +17,7 @@ aes = "0.6" base64 = "0.13" byteorder = "1.4" bytes = "1" +chrono = "0.4" form_urlencoded = "1.0" futures-core = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "unstable", "sink"] } @@ -38,11 +39,13 @@ priority-queue = "1.1" protobuf = "2.14.0" quick-xml = { version = "0.22", features = [ "serialize" ] } rand = "0.8" +rustls = "0.19" +rustls-native-certs = "0.5" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha-1 = "0.9" shannon = "0.2.0" -thiserror = "1.0.7" +thiserror = "1.0" tokio = { version = "1.5", features = ["io-util", "macros", "net", "rt", "time", "sync"] } tokio-stream = "0.1.1" tokio-tungstenite = { version = "0.14", default-features = false, features = ["rustls-tls"] } diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index d39c3101..e78a272c 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,4 +1,4 @@ -use hyper::{Body, Request}; +use hyper::{Body, Method, Request}; use serde::Deserialize; use std::error::Error; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -69,7 +69,7 @@ impl ApResolver { pub async fn try_apresolve(&self) -> Result> { let req = Request::builder() - .method("GET") + .method(Method::GET) .uri("http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient") .body(Body::empty())?; diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs new file mode 100644 index 00000000..6d87cac9 --- /dev/null +++ b/core/src/cdn_url.rs @@ -0,0 +1,151 @@ +use chrono::Local; +use protobuf::{Message, ProtobufError}; +use thiserror::Error; +use url::Url; + +use std::convert::{TryFrom, TryInto}; +use std::ops::{Deref, DerefMut}; + +use super::date::Date; +use super::file_id::FileId; +use super::session::Session; +use super::spclient::SpClientError; + +use librespot_protocol as protocol; +use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage; +use protocol::storage_resolve::StorageResolveResponse_Result; + +#[derive(Error, Debug)] +pub enum CdnUrlError { + #[error("no URLs available")] + Empty, + #[error("all tokens expired")] + Expired, + #[error("error parsing response")] + Parsing, + #[error("could not parse protobuf: {0}")] + Protobuf(#[from] ProtobufError), + #[error("could not complete API request: {0}")] + SpClient(#[from] SpClientError), +} + +#[derive(Debug, Clone)] +pub struct MaybeExpiringUrl(pub String, pub Option); + +#[derive(Debug, Clone)] +pub struct MaybeExpiringUrls(pub Vec); + +impl Deref for MaybeExpiringUrls { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for MaybeExpiringUrls { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Clone)] +pub struct CdnUrl { + pub file_id: FileId, + pub urls: MaybeExpiringUrls, +} + +impl CdnUrl { + pub fn new(file_id: FileId) -> Self { + Self { + file_id, + urls: MaybeExpiringUrls(Vec::new()), + } + } + + pub async fn resolve_audio(&self, session: &Session) -> Result { + let file_id = self.file_id; + let response = session.spclient().get_audio_urls(file_id).await?; + let msg = CdnUrlMessage::parse_from_bytes(&response)?; + let urls = MaybeExpiringUrls::try_from(msg)?; + + let cdn_url = Self { file_id, urls }; + + trace!("Resolved CDN storage: {:#?}", cdn_url); + + Ok(cdn_url) + } + + pub fn get_url(&mut self) -> Result<&str, CdnUrlError> { + if self.urls.is_empty() { + return Err(CdnUrlError::Empty); + } + + // remove expired URLs until the first one is current, or none are left + let now = Local::now(); + while !self.urls.is_empty() { + let maybe_expiring = self.urls[0].1; + if let Some(expiry) = maybe_expiring { + if now < expiry.as_utc() { + break; + } else { + self.urls.remove(0); + } + } + } + + if let Some(cdn_url) = self.urls.first() { + Ok(&cdn_url.0) + } else { + Err(CdnUrlError::Expired) + } + } +} + +impl TryFrom for MaybeExpiringUrls { + type Error = CdnUrlError; + fn try_from(msg: CdnUrlMessage) -> Result { + if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) { + return Err(CdnUrlError::Parsing); + } + + let is_expiring = !msg.get_fileid().is_empty(); + + let result = msg + .get_cdnurl() + .iter() + .map(|cdn_url| { + let url = Url::parse(cdn_url).map_err(|_| CdnUrlError::Parsing)?; + + if is_expiring { + let expiry_str = if let Some(token) = url + .query_pairs() + .into_iter() + .find(|(key, _value)| key == "__token__") + { + let start = token.1.find("exp=").ok_or(CdnUrlError::Parsing)?; + let slice = &token.1[start + 4..]; + let end = slice.find('~').ok_or(CdnUrlError::Parsing)?; + String::from(&slice[..end]) + } else if let Some(query) = url.query() { + let mut items = query.split('_'); + String::from(items.next().ok_or(CdnUrlError::Parsing)?) + } else { + return Err(CdnUrlError::Parsing); + }; + + let mut expiry: i64 = expiry_str.parse().map_err(|_| CdnUrlError::Parsing)?; + expiry -= 5 * 60; // seconds + + Ok(MaybeExpiringUrl( + cdn_url.to_owned(), + Some(expiry.try_into().map_err(|_| CdnUrlError::Parsing)?), + )) + } else { + Ok(MaybeExpiringUrl(cdn_url.to_owned(), None)) + } + }) + .collect::, CdnUrlError>>()?; + + Ok(Self(result)) + } +} diff --git a/metadata/src/date.rs b/core/src/date.rs similarity index 85% rename from metadata/src/date.rs rename to core/src/date.rs index c402c05f..a84da606 100644 --- a/metadata/src/date.rs +++ b/core/src/date.rs @@ -4,13 +4,17 @@ use std::ops::Deref; use chrono::{DateTime, Utc}; use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; - -use crate::error::MetadataError; +use thiserror::Error; use librespot_protocol as protocol; - use protocol::metadata::Date as DateMessage; +#[derive(Debug, Error)] +pub enum DateError { + #[error("item has invalid date")] + InvalidTimestamp, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Date(pub DateTime); @@ -26,11 +30,11 @@ impl Date { self.0.timestamp() } - pub fn from_timestamp(timestamp: i64) -> Result { + pub fn from_timestamp(timestamp: i64) -> Result { if let Some(date_time) = NaiveDateTime::from_timestamp_opt(timestamp, 0) { Ok(Self::from_utc(date_time)) } else { - Err(MetadataError::InvalidTimestamp) + Err(DateError::InvalidTimestamp) } } @@ -63,7 +67,7 @@ impl From> for Date { } impl TryFrom for Date { - type Error = MetadataError; + type Error = DateError; fn try_from(timestamp: i64) -> Result { Self::from_timestamp(timestamp) } diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 7b8aad72..21624e1a 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,10 +1,14 @@ use bytes::Bytes; +use futures_util::future::IntoStream; +use futures_util::FutureExt; use http::header::HeaderValue; use http::uri::InvalidUri; -use hyper::header::InvalidHeaderValue; +use hyper::client::{HttpConnector, ResponseFuture}; +use hyper::header::{InvalidHeaderValue, USER_AGENT}; use hyper::{Body, Client, Request, Response, StatusCode}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::HttpsConnector; +use rustls::ClientConfig; use std::env::consts::OS; use thiserror::Error; use url::Url; @@ -13,6 +17,7 @@ use crate::version::{SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING}; pub struct HttpClient { proxy: Option, + tls_config: ClientConfig, } #[derive(Error, Debug)] @@ -43,15 +48,60 @@ impl From for HttpClientError { impl HttpClient { pub fn new(proxy: Option<&Url>) -> Self { + // configuring TLS is expensive and should be done once per process + let root_store = match rustls_native_certs::load_native_certs() { + Ok(store) => store, + Err((Some(store), err)) => { + warn!("Could not load all certificates: {:?}", err); + store + } + Err((None, err)) => Err(err).expect("cannot access native cert store"), + }; + + let mut tls_config = ClientConfig::new(); + tls_config.root_store = root_store; + tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + Self { proxy: proxy.cloned(), + tls_config, } } - pub async fn request(&self, mut req: Request) -> Result, HttpClientError> { + pub async fn request(&self, req: Request) -> Result, HttpClientError> { + let request = self.request_fut(req)?; + { + let response = request.await; + if let Ok(response) = &response { + let status = response.status(); + if status != StatusCode::OK { + return Err(HttpClientError::NotOK(status.into())); + } + } + response.map_err(HttpClientError::Response) + } + } + + pub async fn request_body(&self, req: Request) -> Result { + let response = self.request(req).await?; + hyper::body::to_bytes(response.into_body()) + .await + .map_err(HttpClientError::Response) + } + + pub fn request_stream( + &self, + req: Request, + ) -> Result, HttpClientError> { + Ok(self.request_fut(req)?.into_stream()) + } + + pub fn request_fut(&self, mut req: Request) -> Result { trace!("Requesting {:?}", req.uri().to_string()); - let connector = HttpsConnector::with_native_roots(); + let mut http = HttpConnector::new(); + http.enforce_http(false); + let connector = HttpsConnector::from((http, self.tls_config.clone())); let spotify_version = match OS { "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), @@ -68,7 +118,7 @@ impl HttpClient { let headers_mut = req.headers_mut(); headers_mut.insert( - "User-Agent", + USER_AGENT, // Some features like lyrics are version-gated and require an official version string. HeaderValue::from_str(&format!( "Spotify/{} {} ({})", @@ -76,38 +126,16 @@ impl HttpClient { ))?, ); - let response = if let Some(url) = &self.proxy { + let request = if let Some(url) = &self.proxy { let proxy_uri = url.to_string().parse()?; let proxy = Proxy::new(Intercept::All, proxy_uri); let proxy_connector = ProxyConnector::from_proxy(connector, proxy)?; - Client::builder() - .build(proxy_connector) - .request(req) - .await - .map_err(HttpClientError::Request) + Client::builder().build(proxy_connector).request(req) } else { - Client::builder() - .build(connector) - .request(req) - .await - .map_err(HttpClientError::Request) + Client::builder().build(connector).request(req) }; - if let Ok(response) = &response { - let status = response.status(); - if status != StatusCode::OK { - return Err(HttpClientError::NotOK(status.into())); - } - } - - response - } - - pub async fn request_body(&self, req: Request) -> Result { - let response = self.request(req).await?; - hyper::body::to_bytes(response.into_body()) - .await - .map_err(HttpClientError::Response) + Ok(request) } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 09275d80..76ddbd37 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -11,9 +11,11 @@ pub mod apresolve; pub mod audio_key; pub mod authentication; pub mod cache; +pub mod cdn_url; pub mod channel; pub mod config; mod connection; +pub mod date; #[allow(dead_code)] mod dealer; #[doc(hidden)] diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 3a40c1a7..c0336690 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -8,9 +8,11 @@ use crate::protocol::extended_metadata::BatchedEntityRequest; use crate::spotify_id::SpotifyId; use bytes::Bytes; +use futures_util::future::IntoStream; use http::header::HeaderValue; -use hyper::header::InvalidHeaderValue; -use hyper::{Body, HeaderMap, Request}; +use hyper::client::ResponseFuture; +use hyper::header::{InvalidHeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}; +use hyper::{Body, HeaderMap, Method, Request}; use protobuf::Message; use rand::Rng; use std::time::Duration; @@ -86,7 +88,7 @@ impl SpClient { pub async fn request_with_protobuf( &self, - method: &str, + method: &Method, endpoint: &str, headers: Option, message: &dyn Message, @@ -94,7 +96,7 @@ impl SpClient { let body = protobuf::text_format::print_to_string(message); let mut headers = headers.unwrap_or_else(HeaderMap::new); - headers.insert("Content-Type", "application/protobuf".parse()?); + headers.insert(CONTENT_TYPE, "application/protobuf".parse()?); self.request(method, endpoint, Some(headers), Some(body)) .await @@ -102,20 +104,20 @@ impl SpClient { pub async fn request_as_json( &self, - method: &str, + method: &Method, endpoint: &str, headers: Option, body: Option, ) -> SpClientResult { let mut headers = headers.unwrap_or_else(HeaderMap::new); - headers.insert("Accept", "application/json".parse()?); + headers.insert(ACCEPT, "application/json".parse()?); self.request(method, endpoint, Some(headers), body).await } pub async fn request( &self, - method: &str, + method: &Method, endpoint: &str, headers: Option, body: Option, @@ -130,12 +132,12 @@ impl SpClient { // Reconnection logic: retrieve the endpoint every iteration, so we can try // another access point when we are experiencing network issues (see below). - let mut uri = self.base_url().await; - uri.push_str(endpoint); + let mut url = self.base_url().await; + url.push_str(endpoint); let mut request = Request::builder() .method(method) - .uri(uri) + .uri(url) .body(Body::from(body.clone()))?; // Reconnection logic: keep getting (cached) tokens because they might have expired. @@ -144,7 +146,7 @@ impl SpClient { *headers_mut = hdrs.clone(); } headers_mut.insert( - "Authorization", + AUTHORIZATION, HeaderValue::from_str(&format!( "Bearer {}", self.session() @@ -212,13 +214,13 @@ impl SpClient { let mut headers = HeaderMap::new(); headers.insert("X-Spotify-Connection-Id", connection_id.parse()?); - self.request_with_protobuf("PUT", &endpoint, Some(headers), &state) + self.request_with_protobuf(&Method::PUT, &endpoint, Some(headers), &state) .await } pub async fn get_metadata(&self, scope: &str, id: SpotifyId) -> SpClientResult { let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()); - self.request("GET", &endpoint, None, None).await + self.request(&Method::GET, &endpoint, None, None).await } pub async fn get_track_metadata(&self, track_id: SpotifyId) -> SpClientResult { @@ -244,7 +246,8 @@ impl SpClient { pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult { let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62(),); - self.request_as_json("GET", &endpoint, None, None).await + self.request_as_json(&Method::GET, &endpoint, None, None) + .await } pub async fn get_lyrics_for_image( @@ -258,19 +261,48 @@ impl SpClient { image_id ); - self.request_as_json("GET", &endpoint, None, None).await + self.request_as_json(&Method::GET, &endpoint, None, None) + .await } // TODO: Find endpoint for newer canvas.proto and upgrade to that. pub async fn get_canvases(&self, request: EntityCanvazRequest) -> SpClientResult { let endpoint = "/canvaz-cache/v0/canvases"; - self.request_with_protobuf("POST", endpoint, None, &request) + self.request_with_protobuf(&Method::POST, endpoint, None, &request) .await } pub async fn get_extended_metadata(&self, request: BatchedEntityRequest) -> SpClientResult { let endpoint = "/extended-metadata/v0/extended-metadata"; - self.request_with_protobuf("POST", endpoint, None, &request) + self.request_with_protobuf(&Method::POST, endpoint, None, &request) .await } + + pub async fn get_audio_urls(&self, file_id: FileId) -> SpClientResult { + let endpoint = format!( + "/storage-resolve/files/audio/interactive/{}", + file_id.to_base16() + ); + self.request(&Method::GET, &endpoint, None, None).await + } + + pub fn stream_file( + &self, + url: &str, + offset: usize, + length: usize, + ) -> Result, SpClientError> { + let req = Request::builder() + .method(&Method::GET) + .uri(url) + .header( + RANGE, + HeaderValue::from_str(&format!("bytes={}-{}", offset, offset + length - 1))?, + ) + .body(Body::empty())?; + + let stream = self.session().http_client().request_stream(req)?; + + Ok(stream) + } } diff --git a/metadata/src/album.rs b/metadata/src/album.rs index fe01ee2b..ac6fec20 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -6,7 +6,6 @@ use crate::{ artist::Artists, availability::Availabilities, copyright::Copyrights, - date::Date, error::{MetadataError, RequestError}, external_id::ExternalIds, image::Images, @@ -18,6 +17,7 @@ use crate::{ Metadata, }; +use librespot_core::date::Date; use librespot_core::session::Session; use librespot_core::spotify_id::SpotifyId; use librespot_protocol as protocol; diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index eb2b5fdd..27a85eed 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -3,8 +3,9 @@ use std::ops::Deref; use thiserror::Error; -use crate::{date::Date, util::from_repeated_message}; +use crate::util::from_repeated_message; +use librespot_core::date::Date; use librespot_protocol as protocol; use protocol::metadata::Availability as AvailabilityMessage; diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 7032999b..05d68aaf 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -9,7 +9,6 @@ use crate::{ }, availability::Availabilities, content_rating::ContentRatings, - date::Date, error::{MetadataError, RequestError}, image::Images, request::RequestResult, @@ -19,6 +18,7 @@ use crate::{ Metadata, }; +use librespot_core::date::Date; use librespot_core::session::Session; use librespot_core::spotify_id::SpotifyId; use librespot_protocol as protocol; diff --git a/metadata/src/error.rs b/metadata/src/error.rs index 2aeaef1e..d1f6cc0b 100644 --- a/metadata/src/error.rs +++ b/metadata/src/error.rs @@ -3,6 +3,7 @@ use thiserror::Error; use protobuf::ProtobufError; +use librespot_core::date::DateError; use librespot_core::mercury::MercuryError; use librespot_core::spclient::SpClientError; use librespot_core::spotify_id::SpotifyIdError; @@ -22,7 +23,7 @@ pub enum MetadataError { #[error("{0}")] InvalidSpotifyId(#[from] SpotifyIdError), #[error("item has invalid date")] - InvalidTimestamp, + InvalidTimestamp(#[from] DateError), #[error("audio item is non-playable")] NonPlayable, #[error("could not parse protobuf: {0}")] diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 15b68e1f..af9c80ec 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -15,7 +15,6 @@ pub mod audio; pub mod availability; pub mod content_rating; pub mod copyright; -pub mod date; pub mod episode; pub mod error; pub mod external_id; diff --git a/metadata/src/playlist/attribute.rs b/metadata/src/playlist/attribute.rs index f00a2b13..ac2eef65 100644 --- a/metadata/src/playlist/attribute.rs +++ b/metadata/src/playlist/attribute.rs @@ -3,8 +3,9 @@ use std::convert::{TryFrom, TryInto}; use std::fmt::Debug; use std::ops::Deref; -use crate::{date::Date, error::MetadataError, image::PictureSizes, util::from_repeated_enum}; +use crate::{error::MetadataError, image::PictureSizes, util::from_repeated_enum}; +use librespot_core::date::Date; use librespot_core::spotify_id::SpotifyId; use librespot_protocol as protocol; diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index de2dc6db..5b97c382 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -2,10 +2,11 @@ use std::convert::{TryFrom, TryInto}; use std::fmt::Debug; use std::ops::Deref; -use crate::{date::Date, error::MetadataError, util::try_from_repeated_message}; +use crate::{error::MetadataError, util::try_from_repeated_message}; use super::attribute::{PlaylistAttributes, PlaylistItemAttributes}; +use librespot_core::date::Date; use librespot_core::spotify_id::SpotifyId; use librespot_protocol as protocol; diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 625373db..5df839b1 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -5,7 +5,6 @@ use std::ops::Deref; use protobuf::Message; use crate::{ - date::Date, error::MetadataError, request::{MercuryRequest, RequestResult}, util::{from_repeated_enum, try_from_repeated_message}, @@ -17,6 +16,7 @@ use super::{ permission::Capabilities, }; +use librespot_core::date::Date; use librespot_core::session::Session; use librespot_core::spotify_id::{NamedSpotifyId, SpotifyId}; use librespot_protocol as protocol; diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs index 6152b901..9040d71e 100644 --- a/metadata/src/sale_period.rs +++ b/metadata/src/sale_period.rs @@ -1,8 +1,9 @@ use std::fmt::Debug; use std::ops::Deref; -use crate::{date::Date, restriction::Restrictions, util::from_repeated_message}; +use crate::{restriction::Restrictions, util::from_repeated_message}; +use librespot_core::date::Date; use librespot_protocol as protocol; use protocol::metadata::SalePeriod as SalePeriodMessage; diff --git a/metadata/src/track.rs b/metadata/src/track.rs index d0639c82..fc9c131e 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -13,7 +13,6 @@ use crate::{ }, availability::{Availabilities, UnavailabilityReason}, content_rating::ContentRatings, - date::Date, error::RequestError, external_id::ExternalIds, restriction::Restrictions, @@ -22,6 +21,7 @@ use crate::{ Metadata, MetadataError, RequestResult, }; +use librespot_core::date::Date; use librespot_core::session::Session; use librespot_core::spotify_id::SpotifyId; use librespot_protocol as protocol; diff --git a/playback/src/player.rs b/playback/src/player.rs index 6dec6a56..50493185 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -341,8 +341,6 @@ impl Player { // While PlayerInternal is written as a future, it still contains blocking code. // It must be run by using block_on() in a dedicated thread. - // futures_executor::block_on(internal); - let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); runtime.block_on(internal); @@ -1904,7 +1902,7 @@ impl PlayerInternal { let (result_tx, result_rx) = oneshot::channel(); let handle = tokio::runtime::Handle::current(); - std::thread::spawn(move || { + thread::spawn(move || { let data = handle.block_on(loader.load_track(spotify_id, position_ms)); if let Some(data) = data { let _ = result_tx.send(data); diff --git a/protocol/build.rs b/protocol/build.rs index a4ca4c37..aa107607 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -26,6 +26,7 @@ fn compile() { proto_dir.join("playlist_annotate3.proto"), proto_dir.join("playlist_permission.proto"), proto_dir.join("playlist4_external.proto"), + proto_dir.join("storage-resolve.proto"), proto_dir.join("user_attributes.proto"), // TODO: remove these legacy protobufs when we are on the new API completely proto_dir.join("authentication.proto"), From 97d4d83b7c208a93e75d9ee1842076e594bccae4 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 16 Dec 2021 23:03:30 +0100 Subject: [PATCH 78/95] cargo fmt --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index e3a529f5..0dce723a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1654,4 +1654,4 @@ async fn main() { } } } -} \ No newline at end of file +} From 3b07a6bcb98650e60cb12a529a0646237990a474 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 17 Dec 2021 20:58:05 +0100 Subject: [PATCH 79/95] Support user-defined temp directories --- audio/src/fetch/mod.rs | 3 +-- core/src/config.rs | 3 +++ src/main.rs | 25 ++++++++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 97037d6e..c4f6c72f 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -406,8 +406,7 @@ impl AudioFileStreaming { read_position: AtomicUsize::new(0), }); - // TODO : use new_in() to store securely in librespot directory - let write_file = NamedTempFile::new().unwrap(); + let write_file = NamedTempFile::new_in(session.config().tmp_dir.clone()).unwrap(); let read_file = write_file.reopen().unwrap(); let (stream_loader_command_tx, stream_loader_command_rx) = diff --git a/core/src/config.rs b/core/src/config.rs index b8c448c2..24b6a88e 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::path::PathBuf; use std::str::FromStr; use url::Url; @@ -8,6 +9,7 @@ pub struct SessionConfig { pub device_id: String, pub proxy: Option, pub ap_port: Option, + pub tmp_dir: PathBuf, } impl Default for SessionConfig { @@ -18,6 +20,7 @@ impl Default for SessionConfig { device_id, proxy: None, ap_port: None, + tmp_dir: std::env::temp_dir(), } } } diff --git a/src/main.rs b/src/main.rs index 0dce723a..8ff9f8b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,8 +26,9 @@ mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; use std::env; +use std::fs::create_dir_all; use std::ops::RangeInclusive; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::pin::Pin; use std::process::exit; use std::str::FromStr; @@ -228,6 +229,7 @@ fn get_setup() -> Setup { const PROXY: &str = "proxy"; const QUIET: &str = "quiet"; const SYSTEM_CACHE: &str = "system-cache"; + const TEMP_DIR: &str = "tmp"; const USERNAME: &str = "username"; const VERBOSE: &str = "verbose"; const VERSION: &str = "version"; @@ -266,6 +268,7 @@ fn get_setup() -> Setup { const ALSA_MIXER_DEVICE_SHORT: &str = "S"; const ALSA_MIXER_INDEX_SHORT: &str = "s"; const ALSA_MIXER_CONTROL_SHORT: &str = "T"; + const TEMP_DIR_SHORT: &str = "t"; const NORMALISATION_ATTACK_SHORT: &str = "U"; const USERNAME_SHORT: &str = "u"; const VERSION_SHORT: &str = "V"; @@ -279,7 +282,7 @@ fn get_setup() -> Setup { const NORMALISATION_THRESHOLD_SHORT: &str = "Z"; const ZEROCONF_PORT_SHORT: &str = "z"; - // Options that have different desc's + // Options that have different descriptions // depending on what backends were enabled at build time. #[cfg(feature = "alsa-backend")] const MIXER_TYPE_DESC: &str = "Mixer to use {alsa|softvol}. Defaults to softvol."; @@ -411,10 +414,16 @@ fn get_setup() -> Setup { "Displayed device type. Defaults to speaker.", "TYPE", ) + .optopt( + TEMP_DIR_SHORT, + TEMP_DIR, + "Path to a directory where files will be temporarily stored while downloading.", + "PATH", + ) .optopt( CACHE_SHORT, CACHE, - "Path to a directory where files will be cached.", + "Path to a directory where files will be cached after downloading.", "PATH", ) .optopt( @@ -912,6 +921,15 @@ fn get_setup() -> Setup { } }; + let tmp_dir = opt_str(TEMP_DIR).map_or(SessionConfig::default().tmp_dir, |p| { + let tmp_dir = PathBuf::from(p); + if let Err(e) = create_dir_all(&tmp_dir) { + error!("could not create or access specified tmp directory: {}", e); + exit(1); + } + tmp_dir + }); + let cache = { let volume_dir = opt_str(SYSTEM_CACHE) .or_else(|| opt_str(CACHE)) @@ -1162,6 +1180,7 @@ fn get_setup() -> Setup { exit(1); } }), + tmp_dir, }; let player_config = { From 9d88ac59c63ec373345681f60fcf3eec6bdf369a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 18 Dec 2021 12:39:16 +0100 Subject: [PATCH 80/95] Configure User-Agent once --- audio/src/fetch/mod.rs | 2 ++ core/src/config.rs | 2 -- core/src/http_client.rs | 71 ++++++++++++++++++++++------------------- src/main.rs | 1 - 4 files changed, 40 insertions(+), 36 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index c4f6c72f..d60f5861 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -363,6 +363,8 @@ impl AudioFileStreaming { let mut cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?; let url = cdn_url.get_url()?; + trace!("Streaming {:?}", url); + let mut streamer = session.spclient().stream_file(url, 0, download_size)?; let request_time = Instant::now(); diff --git a/core/src/config.rs b/core/src/config.rs index 24b6a88e..c6b3d23c 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -5,7 +5,6 @@ use url::Url; #[derive(Clone, Debug)] pub struct SessionConfig { - pub user_agent: String, pub device_id: String, pub proxy: Option, pub ap_port: Option, @@ -16,7 +15,6 @@ impl Default for SessionConfig { fn default() -> SessionConfig { let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string(); SessionConfig { - user_agent: crate::version::VERSION_STRING.to_string(), device_id, proxy: None, ap_port: None, diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 21624e1a..ebd7aefd 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -4,18 +4,20 @@ use futures_util::FutureExt; use http::header::HeaderValue; use http::uri::InvalidUri; use hyper::client::{HttpConnector, ResponseFuture}; -use hyper::header::{InvalidHeaderValue, USER_AGENT}; +use hyper::header::USER_AGENT; use hyper::{Body, Client, Request, Response, StatusCode}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::HttpsConnector; -use rustls::ClientConfig; -use std::env::consts::OS; +use rustls::{ClientConfig, RootCertStore}; use thiserror::Error; use url::Url; +use std::env::consts::OS; + use crate::version::{SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING}; pub struct HttpClient { + user_agent: HeaderValue, proxy: Option, tls_config: ClientConfig, } @@ -34,12 +36,6 @@ pub enum HttpClientError { ProxyBuilder(#[from] std::io::Error), } -impl From for HttpClientError { - fn from(err: InvalidHeaderValue) -> Self { - Self::Parsing(err.into()) - } -} - impl From for HttpClientError { fn from(err: InvalidUri) -> Self { Self::Parsing(err.into()) @@ -48,6 +44,30 @@ impl From for HttpClientError { impl HttpClient { pub fn new(proxy: Option<&Url>) -> Self { + let spotify_version = match OS { + "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), + _ => SPOTIFY_VERSION.to_string(), + }; + + let spotify_platform = match OS { + "android" => "Android/31", + "ios" => "iOS/15.1.1", + "macos" => "OSX/0", + "windows" => "Win32/0", + _ => "Linux/0", + }; + + let user_agent_str = &format!( + "Spotify/{} {} ({})", + spotify_version, spotify_platform, VERSION_STRING + ); + + let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| { + error!("Invalid user agent <{}>: {}", user_agent_str, err); + error!("Parts of the API will probably not work. Please report this as a bug."); + HeaderValue::from_static("") + }); + // configuring TLS is expensive and should be done once per process let root_store = match rustls_native_certs::load_native_certs() { Ok(store) => store, @@ -55,7 +75,11 @@ impl HttpClient { warn!("Could not load all certificates: {:?}", err); store } - Err((None, err)) => Err(err).expect("cannot access native cert store"), + Err((None, err)) => { + error!("Cannot access native certificate store: {}", err); + error!("Continuing, but most requests will probably fail until you fix your system certificate store."); + RootCertStore::empty() + } }; let mut tls_config = ClientConfig::new(); @@ -63,12 +87,15 @@ impl HttpClient { tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; Self { + user_agent, proxy: proxy.cloned(), tls_config, } } pub async fn request(&self, req: Request) -> Result, HttpClientError> { + debug!("Requesting {:?}", req.uri().to_string()); + let request = self.request_fut(req)?; { let response = request.await; @@ -97,34 +124,12 @@ impl HttpClient { } pub fn request_fut(&self, mut req: Request) -> Result { - trace!("Requesting {:?}", req.uri().to_string()); - let mut http = HttpConnector::new(); http.enforce_http(false); let connector = HttpsConnector::from((http, self.tls_config.clone())); - let spotify_version = match OS { - "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), - _ => SPOTIFY_VERSION.to_string(), - }; - - let spotify_platform = match OS { - "android" => "Android/31", - "ios" => "iOS/15.1.1", - "macos" => "OSX/0", - "windows" => "Win32/0", - _ => "Linux/0", - }; - let headers_mut = req.headers_mut(); - headers_mut.insert( - USER_AGENT, - // Some features like lyrics are version-gated and require an official version string. - HeaderValue::from_str(&format!( - "Spotify/{} {} ({})", - spotify_version, spotify_platform, VERSION_STRING - ))?, - ); + headers_mut.insert(USER_AGENT, self.user_agent.clone()); let request = if let Some(url) = &self.proxy { let proxy_uri = url.to_string().parse()?; diff --git a/src/main.rs b/src/main.rs index 8ff9f8b6..6bfb027b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1151,7 +1151,6 @@ fn get_setup() -> Setup { }; let session_config = SessionConfig { - user_agent: version::VERSION_STRING.to_string(), device_id: device_id(&connect_config.name), proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( |s| { From d18a0d1803d5090164d94ef96ded02ef100a3982 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 18 Dec 2021 14:02:28 +0100 Subject: [PATCH 81/95] Fix caching message when cache is disabled --- audio/src/fetch/mod.rs | 10 ++++++---- core/src/cache.rs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index d60f5861..50029840 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -310,10 +310,12 @@ impl AudioFile { let session_ = session.clone(); session.spawn(complete_rx.map_ok(move |mut file| { if let Some(cache) = session_.cache() { - debug!("File {} complete, saving to cache", file_id); - cache.save_file(file_id, &mut file); - } else { - debug!("File {} complete", file_id); + if cache.file_path(file_id).is_some() { + cache.save_file(file_id, &mut file); + debug!("File {} cached to {:?}", file_id, cache.file(file_id)); + } + + debug!("Downloading file {} complete", file_id); } })); diff --git a/core/src/cache.rs b/core/src/cache.rs index 7d85bd6a..af92ab78 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -350,7 +350,7 @@ impl Cache { } } - fn file_path(&self, file: FileId) -> Option { + pub fn file_path(&self, file: FileId) -> Option { self.audio_location.as_ref().map(|location| { let name = file.to_base16(); let mut path = location.join(&name[0..2]); From 0d51fd43dce3206945b8c7bfb98e6a6e19a633f9 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 18 Dec 2021 23:44:13 +0100 Subject: [PATCH 82/95] Remove unwraps from librespot-audio --- audio/src/fetch/mod.rs | 118 +++++++++++++++------- audio/src/fetch/receive.rs | 200 ++++++++++++++++++++++++------------- audio/src/lib.rs | 2 +- audio/src/range_set.rs | 8 +- core/src/cache.rs | 32 +++--- core/src/cdn_url.rs | 2 +- core/src/http_client.rs | 8 +- core/src/version.rs | 3 + playback/src/player.rs | 93 +++++++++++------ 9 files changed, 301 insertions(+), 165 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 50029840..09db431f 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -25,16 +25,28 @@ use self::receive::audio_file_fetch; use crate::range_set::{Range, RangeSet}; +pub type AudioFileResult = Result<(), AudioFileError>; + #[derive(Error, Debug)] pub enum AudioFileError { #[error("could not complete CDN request: {0}")] - Cdn(hyper::Error), + Cdn(#[from] hyper::Error), + #[error("channel was disconnected")] + Channel, #[error("empty response")] Empty, + #[error("I/O error: {0}")] + Io(#[from] io::Error), + #[error("output file unavailable")] + Output, #[error("error parsing response")] Parsing, + #[error("mutex was poisoned")] + Poisoned, #[error("could not complete API request: {0}")] SpClient(#[from] SpClientError), + #[error("streamer did not report progress")] + Timeout, #[error("could not get CDN URL: {0}")] Url(#[from] CdnUrlError), } @@ -42,7 +54,7 @@ pub enum AudioFileError { /// The minimum size of a block that is requested from the Spotify servers in one request. /// This is the block size that is typically requested while doing a `seek()` on a file. /// Note: smaller requests can happen if part of the block is downloaded already. -pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 256; +pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128; /// The amount of data that is requested when initially opening a file. /// Note: if the file is opened to play from the beginning, the amount of data to @@ -142,23 +154,32 @@ impl StreamLoaderController { self.file_size == 0 } - pub fn range_available(&self, range: Range) -> bool { - if let Some(ref shared) = self.stream_shared { - let download_status = shared.download_status.lock().unwrap(); + pub fn range_available(&self, range: Range) -> Result { + let available = if let Some(ref shared) = self.stream_shared { + let download_status = shared + .download_status + .lock() + .map_err(|_| AudioFileError::Poisoned)?; + range.length <= download_status .downloaded .contained_length_from_value(range.start) } else { range.length <= self.len() - range.start - } + }; + + Ok(available) } - pub fn range_to_end_available(&self) -> bool { - self.stream_shared.as_ref().map_or(true, |shared| { - let read_position = shared.read_position.load(atomic::Ordering::Relaxed); - self.range_available(Range::new(read_position, self.len() - read_position)) - }) + pub fn range_to_end_available(&self) -> Result { + match self.stream_shared { + Some(ref shared) => { + let read_position = shared.read_position.load(atomic::Ordering::Relaxed); + self.range_available(Range::new(read_position, self.len() - read_position)) + } + None => Ok(true), + } } pub fn ping_time(&self) -> Duration { @@ -179,7 +200,7 @@ impl StreamLoaderController { self.send_stream_loader_command(StreamLoaderCommand::Fetch(range)); } - pub fn fetch_blocking(&self, mut range: Range) { + pub fn fetch_blocking(&self, mut range: Range) -> AudioFileResult { // signal the stream loader to tech a range of the file and block until it is loaded. // ensure the range is within the file's bounds. @@ -192,7 +213,11 @@ impl StreamLoaderController { self.fetch(range); if let Some(ref shared) = self.stream_shared { - let mut download_status = shared.download_status.lock().unwrap(); + let mut download_status = shared + .download_status + .lock() + .map_err(|_| AudioFileError::Poisoned)?; + while range.length > download_status .downloaded @@ -201,7 +226,7 @@ impl StreamLoaderController { download_status = shared .cond .wait_timeout(download_status, DOWNLOAD_TIMEOUT) - .unwrap() + .map_err(|_| AudioFileError::Timeout)? .0; if range.length > (download_status @@ -215,6 +240,8 @@ impl StreamLoaderController { } } } + + Ok(()) } pub fn fetch_next(&self, length: usize) { @@ -223,17 +250,20 @@ impl StreamLoaderController { start: shared.read_position.load(atomic::Ordering::Relaxed), length, }; - self.fetch(range) + self.fetch(range); } } - pub fn fetch_next_blocking(&self, length: usize) { - if let Some(ref shared) = self.stream_shared { - let range = Range { - start: shared.read_position.load(atomic::Ordering::Relaxed), - length, - }; - self.fetch_blocking(range); + pub fn fetch_next_blocking(&self, length: usize) -> AudioFileResult { + match self.stream_shared { + Some(ref shared) => { + let range = Range { + start: shared.read_position.load(atomic::Ordering::Relaxed), + length, + }; + self.fetch_blocking(range) + } + None => Ok(()), } } @@ -310,11 +340,9 @@ impl AudioFile { let session_ = session.clone(); session.spawn(complete_rx.map_ok(move |mut file| { if let Some(cache) = session_.cache() { - if cache.file_path(file_id).is_some() { - cache.save_file(file_id, &mut file); + if cache.save_file(file_id, &mut file) { debug!("File {} cached to {:?}", file_id, cache.file(file_id)); } - debug!("Downloading file {} complete", file_id); } })); @@ -322,8 +350,8 @@ impl AudioFile { Ok(AudioFile::Streaming(streaming.await?)) } - pub fn get_stream_loader_controller(&self) -> StreamLoaderController { - match self { + pub fn get_stream_loader_controller(&self) -> Result { + let controller = match self { AudioFile::Streaming(ref stream) => StreamLoaderController { channel_tx: Some(stream.stream_loader_command_tx.clone()), stream_shared: Some(stream.shared.clone()), @@ -332,9 +360,11 @@ impl AudioFile { AudioFile::Cached(ref file) => StreamLoaderController { channel_tx: None, stream_shared: None, - file_size: file.metadata().unwrap().len() as usize, + file_size: file.metadata()?.len() as usize, }, - } + }; + + Ok(controller) } pub fn is_cached(&self) -> bool { @@ -410,8 +440,8 @@ impl AudioFileStreaming { read_position: AtomicUsize::new(0), }); - let write_file = NamedTempFile::new_in(session.config().tmp_dir.clone()).unwrap(); - let read_file = write_file.reopen().unwrap(); + let write_file = NamedTempFile::new_in(session.config().tmp_dir.clone())?; + let read_file = write_file.reopen()?; let (stream_loader_command_tx, stream_loader_command_rx) = mpsc::unbounded_channel::(); @@ -444,7 +474,12 @@ impl Read for AudioFileStreaming { let length = min(output.len(), self.shared.file_size - offset); - let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) { + let length_to_request = match *(self + .shared + .download_strategy + .lock() + .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?) + { DownloadStrategy::RandomAccess() => length, DownloadStrategy::Streaming() => { // Due to the read-ahead stuff, we potentially request more than the actual request demanded. @@ -468,14 +503,18 @@ impl Read for AudioFileStreaming { let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length_to_request)); - let mut download_status = self.shared.download_status.lock().unwrap(); + let mut download_status = self + .shared + .download_status + .lock() + .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?; ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); for &range in ranges_to_request.iter() { self.stream_loader_command_tx .send(StreamLoaderCommand::Fetch(range)) - .unwrap(); + .map_err(|_| io::Error::new(io::ErrorKind::Other, "tx channel is disconnected"))?; } if length == 0 { @@ -484,7 +523,12 @@ impl Read for AudioFileStreaming { let mut download_message_printed = false; while !download_status.downloaded.contains(offset) { - if let DownloadStrategy::Streaming() = *self.shared.download_strategy.lock().unwrap() { + if let DownloadStrategy::Streaming() = *self + .shared + .download_strategy + .lock() + .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))? + { if !download_message_printed { debug!("Stream waiting for download of file position {}. Downloaded ranges: {}. Pending ranges: {}", offset, download_status.downloaded, download_status.requested.minus(&download_status.downloaded)); download_message_printed = true; @@ -494,7 +538,7 @@ impl Read for AudioFileStreaming { .shared .cond .wait_timeout(download_status, DOWNLOAD_TIMEOUT) - .unwrap() + .map_err(|_| io::Error::new(io::ErrorKind::Other, "timeout acquiring mutex"))? .0; } let available_length = download_status @@ -503,7 +547,7 @@ impl Read for AudioFileStreaming { assert!(available_length > 0); drop(download_status); - self.position = self.read_file.seek(SeekFrom::Start(offset as u64)).unwrap(); + self.position = self.read_file.seek(SeekFrom::Start(offset as u64))?; let read_len = min(length, available_length); let read_len = self.read_file.read(&mut output[..read_len])?; diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 6157040f..4eef2b66 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -13,7 +13,10 @@ use librespot_core::session::Session; use crate::range_set::{Range, RangeSet}; -use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand, StreamingRequest}; +use super::{ + AudioFileError, AudioFileResult, AudioFileShared, DownloadStrategy, StreamLoaderCommand, + StreamingRequest, +}; use super::{ FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, @@ -33,7 +36,7 @@ async fn receive_data( shared: Arc, file_data_tx: mpsc::UnboundedSender, mut request: StreamingRequest, -) { +) -> AudioFileResult { let requested_offset = request.offset; let requested_length = request.length; @@ -97,7 +100,10 @@ async fn receive_data( if request_length > 0 { let missing_range = Range::new(data_offset, request_length); - let mut download_status = shared.download_status.lock().unwrap(); + let mut download_status = shared + .download_status + .lock() + .map_err(|_| AudioFileError::Poisoned)?; download_status.requested.subtract_range(&missing_range); shared.cond.notify_all(); } @@ -106,16 +112,23 @@ async fn receive_data( .number_of_open_requests .fetch_sub(1, Ordering::SeqCst); - if let Err(e) = result { - error!( - "Error from streamer for range {} (+{}): {:?}", - requested_offset, requested_length, e - ); - } else if request_length > 0 { - warn!( - "Streamer for range {} (+{}) received less data from server than requested.", - requested_offset, requested_length - ); + match result { + Ok(()) => { + if request_length > 0 { + warn!( + "Streamer for range {} (+{}) received less data from server than requested.", + requested_offset, requested_length + ); + } + Ok(()) + } + Err(e) => { + error!( + "Error from streamer for range {} (+{}): {:?}", + requested_offset, requested_length, e + ); + Err(e.into()) + } } } @@ -137,24 +150,21 @@ enum ControlFlow { } impl AudioFileFetch { - fn get_download_strategy(&mut self) -> DownloadStrategy { - *(self.shared.download_strategy.lock().unwrap()) + fn get_download_strategy(&mut self) -> Result { + let strategy = self + .shared + .download_strategy + .lock() + .map_err(|_| AudioFileError::Poisoned)?; + + Ok(*(strategy)) } - fn download_range(&mut self, offset: usize, mut length: usize) { + fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult { if length < MINIMUM_DOWNLOAD_SIZE { length = MINIMUM_DOWNLOAD_SIZE; } - // ensure the values are within the bounds - if offset >= self.shared.file_size { - return; - } - - if length == 0 { - return; - } - if offset + length > self.shared.file_size { length = self.shared.file_size - offset; } @@ -162,7 +172,11 @@ impl AudioFileFetch { let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length)); - let mut download_status = self.shared.download_status.lock().unwrap(); + let mut download_status = self + .shared + .download_status + .lock() + .map_err(|_| AudioFileError::Poisoned)?; ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); @@ -205,9 +219,15 @@ impl AudioFileFetch { } } } + + Ok(()) } - fn pre_fetch_more_data(&mut self, bytes: usize, max_requests_to_send: usize) { + fn pre_fetch_more_data( + &mut self, + bytes: usize, + max_requests_to_send: usize, + ) -> AudioFileResult { let mut bytes_to_go = bytes; let mut requests_to_go = max_requests_to_send; @@ -216,7 +236,11 @@ impl AudioFileFetch { let mut missing_data = RangeSet::new(); missing_data.add_range(&Range::new(0, self.shared.file_size)); { - let download_status = self.shared.download_status.lock().unwrap(); + let download_status = self + .shared + .download_status + .lock() + .map_err(|_| AudioFileError::Poisoned)?; missing_data.subtract_range_set(&download_status.downloaded); missing_data.subtract_range_set(&download_status.requested); } @@ -234,7 +258,7 @@ impl AudioFileFetch { let range = tail_end.get_range(0); let offset = range.start; let length = min(range.length, bytes_to_go); - self.download_range(offset, length); + self.download_range(offset, length)?; requests_to_go -= 1; bytes_to_go -= length; } else if !missing_data.is_empty() { @@ -242,20 +266,20 @@ impl AudioFileFetch { let range = missing_data.get_range(0); let offset = range.start; let length = min(range.length, bytes_to_go); - self.download_range(offset, length); + self.download_range(offset, length)?; requests_to_go -= 1; bytes_to_go -= length; } else { - return; + break; } } + + Ok(()) } - fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow { + fn handle_file_data(&mut self, data: ReceivedData) -> Result { match data { ReceivedData::ResponseTime(response_time) => { - trace!("Ping time estimated as: {} ms", response_time.as_millis()); - // prune old response times. Keep at most two so we can push a third. while self.network_response_times.len() >= 3 { self.network_response_times.remove(0); @@ -276,24 +300,27 @@ impl AudioFileFetch { _ => unreachable!(), }; + trace!("Ping time estimated as: {} ms", ping_time.as_millis()); + // store our new estimate for everyone to see self.shared .ping_time_ms .store(ping_time.as_millis() as usize, Ordering::Relaxed); } ReceivedData::Data(data) => { - self.output - .as_mut() - .unwrap() - .seek(SeekFrom::Start(data.offset as u64)) - .unwrap(); - self.output - .as_mut() - .unwrap() - .write_all(data.data.as_ref()) - .unwrap(); + match self.output.as_mut() { + Some(output) => { + output.seek(SeekFrom::Start(data.offset as u64))?; + output.write_all(data.data.as_ref())?; + } + None => return Err(AudioFileError::Output), + } - let mut download_status = self.shared.download_status.lock().unwrap(); + let mut download_status = self + .shared + .download_status + .lock() + .map_err(|_| AudioFileError::Poisoned)?; let received_range = Range::new(data.offset, data.data.len()); download_status.downloaded.add_range(&received_range); @@ -305,36 +332,50 @@ impl AudioFileFetch { drop(download_status); if full { - self.finish(); - return ControlFlow::Break; + self.finish()?; + return Ok(ControlFlow::Break); } } } - ControlFlow::Continue + + Ok(ControlFlow::Continue) } - fn handle_stream_loader_command(&mut self, cmd: StreamLoaderCommand) -> ControlFlow { + fn handle_stream_loader_command( + &mut self, + cmd: StreamLoaderCommand, + ) -> Result { match cmd { StreamLoaderCommand::Fetch(request) => { - self.download_range(request.start, request.length); + self.download_range(request.start, request.length)?; } StreamLoaderCommand::RandomAccessMode() => { - *(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::RandomAccess(); + *(self + .shared + .download_strategy + .lock() + .map_err(|_| AudioFileError::Poisoned)?) = DownloadStrategy::RandomAccess(); } StreamLoaderCommand::StreamMode() => { - *(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::Streaming(); + *(self + .shared + .download_strategy + .lock() + .map_err(|_| AudioFileError::Poisoned)?) = DownloadStrategy::Streaming(); } - StreamLoaderCommand::Close() => return ControlFlow::Break, + StreamLoaderCommand::Close() => return Ok(ControlFlow::Break), } - ControlFlow::Continue + Ok(ControlFlow::Continue) } - fn finish(&mut self) { - let mut output = self.output.take().unwrap(); - let complete_tx = self.complete_tx.take().unwrap(); + fn finish(&mut self) -> AudioFileResult { + let mut output = self.output.take().ok_or(AudioFileError::Output)?; + let complete_tx = self.complete_tx.take().ok_or(AudioFileError::Output)?; - output.seek(SeekFrom::Start(0)).unwrap(); - let _ = complete_tx.send(output); + output.seek(SeekFrom::Start(0))?; + complete_tx + .send(output) + .map_err(|_| AudioFileError::Channel) } } @@ -345,7 +386,7 @@ pub(super) async fn audio_file_fetch( output: NamedTempFile, mut stream_loader_command_rx: mpsc::UnboundedReceiver, complete_tx: oneshot::Sender, -) { +) -> AudioFileResult { let (file_data_tx, mut file_data_rx) = mpsc::unbounded_channel(); { @@ -353,7 +394,10 @@ pub(super) async fn audio_file_fetch( initial_request.offset, initial_request.offset + initial_request.length, ); - let mut download_status = shared.download_status.lock().unwrap(); + let mut download_status = shared + .download_status + .lock() + .map_err(|_| AudioFileError::Poisoned)?; download_status.requested.add_range(&requested_range); } @@ -376,25 +420,39 @@ pub(super) async fn audio_file_fetch( loop { tokio::select! { cmd = stream_loader_command_rx.recv() => { - if cmd.map_or(true, |cmd| fetch.handle_stream_loader_command(cmd) == ControlFlow::Break) { - break; + match cmd { + Some(cmd) => { + if fetch.handle_stream_loader_command(cmd)? == ControlFlow::Break { + break; + } + } + None => break, + } } - }, - data = file_data_rx.recv() => { - if data.map_or(true, |data| fetch.handle_file_data(data) == ControlFlow::Break) { - break; + data = file_data_rx.recv() => { + match data { + Some(data) => { + if fetch.handle_file_data(data)? == ControlFlow::Break { + break; + } + } + None => break, } } } - if fetch.get_download_strategy() == DownloadStrategy::Streaming() { + if fetch.get_download_strategy()? == DownloadStrategy::Streaming() { let number_of_open_requests = fetch.shared.number_of_open_requests.load(Ordering::SeqCst); if number_of_open_requests < MAX_PREFETCH_REQUESTS { let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests; let bytes_pending: usize = { - let download_status = fetch.shared.download_status.lock().unwrap(); + let download_status = fetch + .shared + .download_status + .lock() + .map_err(|_| AudioFileError::Poisoned)?; download_status .requested .minus(&download_status.downloaded) @@ -418,9 +476,11 @@ pub(super) async fn audio_file_fetch( fetch.pre_fetch_more_data( desired_pending_bytes - bytes_pending, max_requests_to_send, - ); + )?; } } } } + + Ok(()) } diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 0c96b0d0..5685486d 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -7,7 +7,7 @@ mod fetch; mod range_set; pub use decrypt::AudioDecrypt; -pub use fetch::{AudioFile, StreamLoaderController}; +pub use fetch::{AudioFile, AudioFileError, StreamLoaderController}; pub use fetch::{ READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, diff --git a/audio/src/range_set.rs b/audio/src/range_set.rs index f74058a3..a37b03ae 100644 --- a/audio/src/range_set.rs +++ b/audio/src/range_set.rs @@ -10,7 +10,7 @@ pub struct Range { impl fmt::Display for Range { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - return write!(f, "[{}, {}]", self.start, self.start + self.length - 1); + write!(f, "[{}, {}]", self.start, self.start + self.length - 1) } } @@ -24,16 +24,16 @@ impl Range { } } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct RangeSet { ranges: Vec, } impl fmt::Display for RangeSet { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "(").unwrap(); + write!(f, "(")?; for range in self.ranges.iter() { - write!(f, "{}", range).unwrap(); + write!(f, "{}", range)?; } write!(f, ")") } diff --git a/core/src/cache.rs b/core/src/cache.rs index af92ab78..aec00e84 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -350,7 +350,7 @@ impl Cache { } } - pub fn file_path(&self, file: FileId) -> Option { + fn file_path(&self, file: FileId) -> Option { self.audio_location.as_ref().map(|location| { let name = file.to_base16(); let mut path = location.join(&name[0..2]); @@ -377,24 +377,22 @@ impl Cache { } } - pub fn save_file(&self, file: FileId, contents: &mut F) { - let path = if let Some(path) = self.file_path(file) { - path - } else { - return; - }; - let parent = path.parent().unwrap(); - - let result = fs::create_dir_all(parent) - .and_then(|_| File::create(&path)) - .and_then(|mut file| io::copy(contents, &mut file)); - - if let Ok(size) = result { - if let Some(limiter) = self.size_limiter.as_deref() { - limiter.add(&path, size); - limiter.prune(); + pub fn save_file(&self, file: FileId, contents: &mut F) -> bool { + if let Some(path) = self.file_path(file) { + if let Some(parent) = path.parent() { + if let Ok(size) = fs::create_dir_all(parent) + .and_then(|_| File::create(&path)) + .and_then(|mut file| io::copy(contents, &mut file)) + { + if let Some(limiter) = self.size_limiter.as_deref() { + limiter.add(&path, size); + limiter.prune(); + } + return true; + } } } + false } pub fn remove_file(&self, file: FileId) -> Result<(), RemoveFileError> { diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 6d87cac9..13f23a37 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -80,7 +80,7 @@ impl CdnUrl { return Err(CdnUrlError::Empty); } - // remove expired URLs until the first one is current, or none are left + // prune expired URLs until the first one is current, or none are left let now = Local::now(); while !self.urls.is_empty() { let maybe_expiring = self.urls[0].1; diff --git a/core/src/http_client.rs b/core/src/http_client.rs index ebd7aefd..52206c5c 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -14,7 +14,9 @@ use url::Url; use std::env::consts::OS; -use crate::version::{SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING}; +use crate::version::{ + FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING, +}; pub struct HttpClient { user_agent: HeaderValue, @@ -64,8 +66,8 @@ impl HttpClient { let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| { error!("Invalid user agent <{}>: {}", user_agent_str, err); - error!("Parts of the API will probably not work. Please report this as a bug."); - HeaderValue::from_static("") + error!("Please report this as a bug."); + HeaderValue::from_static(FALLBACK_USER_AGENT) }); // configuring TLS is expensive and should be done once per process diff --git a/core/src/version.rs b/core/src/version.rs index a7e3acd9..98047ef1 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -21,3 +21,6 @@ pub const SPOTIFY_VERSION: u64 = 117300517; /// The protocol version of the Spotify mobile app. pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84"; + +/// The user agent to fall back to, if one could not be determined dynamically. +pub const FALLBACK_USER_AGENT: &str = "Spotify/117300517 Linux/0 (librespot)"; diff --git a/playback/src/player.rs b/playback/src/player.rs index ed4fc055..f0c4acda 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -10,9 +10,10 @@ use std::{mem, thread}; use byteorder::{LittleEndian, ReadBytesExt}; use futures_util::stream::futures_unordered::FuturesUnordered; use futures_util::{future, StreamExt, TryFutureExt}; +use thiserror::Error; use tokio::sync::{mpsc, oneshot}; -use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController}; +use crate::audio::{AudioDecrypt, AudioFile, AudioFileError, StreamLoaderController}; use crate::audio::{ READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, @@ -32,6 +33,14 @@ use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND}; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; pub const DB_VOLTAGE_RATIO: f64 = 20.0; +pub type PlayerResult = Result<(), PlayerError>; + +#[derive(Debug, Error)] +pub enum PlayerError { + #[error("audio file error: {0}")] + AudioFile(#[from] AudioFileError), +} + pub struct Player { commands: Option>, thread_handle: Option>, @@ -216,6 +225,17 @@ pub struct NormalisationData { album_peak: f32, } +impl Default for NormalisationData { + fn default() -> Self { + Self { + track_gain_db: 0.0, + track_peak: 1.0, + album_gain_db: 0.0, + album_peak: 1.0, + } + } +} + impl NormalisationData { fn parse_from_file(mut file: T) -> io::Result { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; @@ -698,19 +718,20 @@ impl PlayerTrackLoader { } fn stream_data_rate(&self, format: AudioFileFormat) -> usize { - match format { - AudioFileFormat::OGG_VORBIS_96 => 12 * 1024, - AudioFileFormat::OGG_VORBIS_160 => 20 * 1024, - AudioFileFormat::OGG_VORBIS_320 => 40 * 1024, - AudioFileFormat::MP3_256 => 32 * 1024, - AudioFileFormat::MP3_320 => 40 * 1024, - AudioFileFormat::MP3_160 => 20 * 1024, - AudioFileFormat::MP3_96 => 12 * 1024, - AudioFileFormat::MP3_160_ENC => 20 * 1024, - AudioFileFormat::AAC_24 => 3 * 1024, - AudioFileFormat::AAC_48 => 6 * 1024, - AudioFileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average - } + let kbps = match format { + AudioFileFormat::OGG_VORBIS_96 => 12, + AudioFileFormat::OGG_VORBIS_160 => 20, + AudioFileFormat::OGG_VORBIS_320 => 40, + AudioFileFormat::MP3_256 => 32, + AudioFileFormat::MP3_320 => 40, + AudioFileFormat::MP3_160 => 20, + AudioFileFormat::MP3_96 => 12, + AudioFileFormat::MP3_160_ENC => 20, + AudioFileFormat::AAC_24 => 3, + AudioFileFormat::AAC_48 => 6, + AudioFileFormat::FLAC_FLAC => 112, // assume 900 kbit/s on average + }; + kbps * 1024 } async fn load_track( @@ -805,9 +826,10 @@ impl PlayerTrackLoader { return None; } }; + let is_cached = encrypted_file.is_cached(); - let stream_loader_controller = encrypted_file.get_stream_loader_controller(); + let stream_loader_controller = encrypted_file.get_stream_loader_controller().ok()?; if play_from_beginning { // No need to seek -> we stream from the beginning @@ -830,13 +852,8 @@ impl PlayerTrackLoader { let normalisation_data = match NormalisationData::parse_from_file(&mut decrypted_file) { Ok(data) => data, Err(_) => { - warn!("Unable to extract normalisation data, using default value."); - NormalisationData { - track_gain_db: 0.0, - track_peak: 1.0, - album_gain_db: 0.0, - album_peak: 1.0, - } + warn!("Unable to extract normalisation data, using default values."); + NormalisationData::default() } }; @@ -929,7 +946,9 @@ impl Future for PlayerInternal { }; if let Some(cmd) = cmd { - self.handle_command(cmd); + if let Err(e) = self.handle_command(cmd) { + error!("Error handling command: {}", e); + } } // Handle loading of a new track to play @@ -1109,7 +1128,9 @@ impl Future for PlayerInternal { if (!*suggested_to_preload_next_track) && ((duration_ms as i64 - Self::position_pcm_to_ms(stream_position_pcm) as i64) < PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64) - && stream_loader_controller.range_to_end_available() + && stream_loader_controller + .range_to_end_available() + .unwrap_or(false) { *suggested_to_preload_next_track = true; self.send_event(PlayerEvent::TimeToPreloadNextTrack { @@ -1785,7 +1806,7 @@ impl PlayerInternal { } } - fn handle_command_seek(&mut self, position_ms: u32) { + fn handle_command_seek(&mut self, position_ms: u32) -> PlayerResult { if let Some(stream_loader_controller) = self.state.stream_loader_controller() { stream_loader_controller.set_random_access_mode(); } @@ -1818,7 +1839,7 @@ impl PlayerInternal { } // ensure we have a bit of a buffer of downloaded data - self.preload_data_before_playback(); + self.preload_data_before_playback()?; if let PlayerState::Playing { track_id, @@ -1851,11 +1872,13 @@ impl PlayerInternal { duration_ms, }); } + + Ok(()) } - fn handle_command(&mut self, cmd: PlayerCommand) { + fn handle_command(&mut self, cmd: PlayerCommand) -> PlayerResult { debug!("command={:?}", cmd); - match cmd { + let result = match cmd { PlayerCommand::Load { track_id, play_request_id, @@ -1865,7 +1888,7 @@ impl PlayerInternal { PlayerCommand::Preload { track_id } => self.handle_command_preload(track_id), - PlayerCommand::Seek(position_ms) => self.handle_command_seek(position_ms), + PlayerCommand::Seek(position_ms) => self.handle_command_seek(position_ms)?, PlayerCommand::Play => self.handle_play(), @@ -1884,7 +1907,9 @@ impl PlayerInternal { PlayerCommand::SetAutoNormaliseAsAlbum(setting) => { self.auto_normalise_as_album = setting } - } + }; + + Ok(result) } fn send_event(&mut self, event: PlayerEvent) { @@ -1928,7 +1953,7 @@ impl PlayerInternal { result_rx.map_err(|_| ()) } - fn preload_data_before_playback(&mut self) { + fn preload_data_before_playback(&mut self) -> Result<(), PlayerError> { if let PlayerState::Playing { bytes_per_second, ref mut stream_loader_controller, @@ -1951,7 +1976,11 @@ impl PlayerInternal { * bytes_per_second as f32) as usize, (READ_AHEAD_BEFORE_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, ); - stream_loader_controller.fetch_next_blocking(wait_for_data_length); + stream_loader_controller + .fetch_next_blocking(wait_for_data_length) + .map_err(|e| e.into()) + } else { + Ok(()) } } } From a297c68913c25379986406b9902bde8d55fe27a4 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 19 Dec 2021 00:11:16 +0100 Subject: [PATCH 83/95] Make ping estimation less chatty --- audio/src/fetch/receive.rs | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 4eef2b66..716c24e1 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -280,6 +280,8 @@ impl AudioFileFetch { fn handle_file_data(&mut self, data: ReceivedData) -> Result { match data { ReceivedData::ResponseTime(response_time) => { + let old_ping_time_ms = self.shared.ping_time_ms.load(Ordering::Relaxed); + // prune old response times. Keep at most two so we can push a third. while self.network_response_times.len() >= 3 { self.network_response_times.remove(0); @@ -289,23 +291,32 @@ impl AudioFileFetch { self.network_response_times.push(response_time); // stats::median is experimental. So we calculate the median of up to three ourselves. - let ping_time = match self.network_response_times.len() { - 1 => self.network_response_times[0], - 2 => (self.network_response_times[0] + self.network_response_times[1]) / 2, - 3 => { - let mut times = self.network_response_times.clone(); - times.sort_unstable(); - times[1] - } - _ => unreachable!(), + let ping_time_ms = { + let response_time = match self.network_response_times.len() { + 1 => self.network_response_times[0], + 2 => (self.network_response_times[0] + self.network_response_times[1]) / 2, + 3 => { + let mut times = self.network_response_times.clone(); + times.sort_unstable(); + times[1] + } + _ => unreachable!(), + }; + response_time.as_millis() as usize }; - trace!("Ping time estimated as: {} ms", ping_time.as_millis()); + // print when the new estimate deviates by more than 10% from the last + if f32::abs( + (ping_time_ms as f32 - old_ping_time_ms as f32) / old_ping_time_ms as f32, + ) > 0.1 + { + debug!("Ping time now estimated as: {} ms", ping_time_ms); + } // store our new estimate for everyone to see self.shared .ping_time_ms - .store(ping_time.as_millis() as usize, Ordering::Relaxed); + .store(ping_time_ms, Ordering::Relaxed); } ReceivedData::Data(data) => { match self.output.as_mut() { From 62461be1fcfcaa93bb52f32cc2f88b7fdcd6ecd7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 26 Dec 2021 21:18:42 +0100 Subject: [PATCH 84/95] Change panics into `Result<_, librespot_core::Error>` --- Cargo.lock | 2 + audio/src/decrypt.rs | 9 +- audio/src/fetch/mod.rs | 177 +++++------ audio/src/fetch/receive.rs | 188 +++++------- audio/src/range_set.rs | 8 +- connect/Cargo.toml | 1 + connect/src/context.rs | 29 +- connect/src/discovery.rs | 11 +- connect/src/spirc.rs | 414 +++++++++++++++----------- core/src/apresolve.rs | 8 +- core/src/audio_key.rs | 101 ++++--- core/src/authentication.rs | 38 ++- core/src/cache.rs | 175 ++++++----- core/src/cdn_url.rs | 119 ++++---- core/src/channel.rs | 49 +++- core/src/component.rs | 2 +- core/src/config.rs | 5 +- core/src/connection/codec.rs | 15 +- core/src/connection/handshake.rs | 42 ++- core/src/connection/mod.rs | 50 ++-- core/src/date.rs | 25 +- core/src/dealer/maps.rs | 23 +- core/src/dealer/mod.rs | 95 +++--- core/src/error.rs | 437 ++++++++++++++++++++++++++++ core/src/file_id.rs | 4 +- core/src/http_client.rs | 126 ++++---- core/src/lib.rs | 7 + core/src/mercury/mod.rs | 99 ++++--- core/src/mercury/sender.rs | 11 +- core/src/mercury/types.rs | 53 ++-- core/src/packet.rs | 2 +- core/src/session.rs | 118 +++++--- core/src/socket.rs | 3 +- core/src/spclient.rs | 65 ++--- core/src/spotify_id.rs | 82 +++--- core/src/token.rs | 36 ++- core/src/util.rs | 18 +- discovery/Cargo.toml | 1 + discovery/src/lib.rs | 24 +- discovery/src/server.rs | 115 +++++--- metadata/src/album.rs | 48 ++- metadata/src/artist.rs | 38 +-- metadata/src/audio/file.rs | 9 +- metadata/src/audio/item.rs | 7 +- metadata/src/availability.rs | 5 +- metadata/src/content_rating.rs | 4 +- metadata/src/copyright.rs | 7 +- metadata/src/episode.rs | 25 +- metadata/src/error.rs | 31 +- metadata/src/external_id.rs | 4 +- metadata/src/image.rs | 23 +- metadata/src/lib.rs | 7 +- metadata/src/playlist/annotation.rs | 12 +- metadata/src/playlist/attribute.rs | 34 +-- metadata/src/playlist/diff.rs | 14 +- metadata/src/playlist/item.rs | 28 +- metadata/src/playlist/list.rs | 30 +- metadata/src/playlist/operation.rs | 19 +- metadata/src/playlist/permission.rs | 4 +- metadata/src/request.rs | 11 +- metadata/src/restriction.rs | 6 +- metadata/src/sale_period.rs | 5 +- metadata/src/show.rs | 29 +- metadata/src/track.rs | 25 +- metadata/src/util.rs | 2 +- metadata/src/video.rs | 7 +- playback/Cargo.toml | 2 +- playback/src/player.rs | 81 +++--- src/main.rs | 68 +++-- 69 files changed, 2041 insertions(+), 1331 deletions(-) create mode 100644 core/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 3e28c806..cce06c16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1246,6 +1246,7 @@ dependencies = [ "rand", "serde", "serde_json", + "thiserror", "tokio", "tokio-stream", ] @@ -1309,6 +1310,7 @@ dependencies = [ "form_urlencoded", "futures", "futures-core", + "futures-util", "hex", "hmac", "hyper", diff --git a/audio/src/decrypt.rs b/audio/src/decrypt.rs index 17f4edba..95dc7c08 100644 --- a/audio/src/decrypt.rs +++ b/audio/src/decrypt.rs @@ -1,8 +1,11 @@ use std::io; -use aes_ctr::cipher::generic_array::GenericArray; -use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek}; -use aes_ctr::Aes128Ctr; +use aes_ctr::{ + cipher::{ + generic_array::GenericArray, NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek, + }, + Aes128Ctr, +}; use librespot_core::audio_key::AudioKey; diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 09db431f..dc5bcdf4 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -1,54 +1,57 @@ mod receive; -use std::cmp::{max, min}; -use std::fs; -use std::io::{self, Read, Seek, SeekFrom}; -use std::sync::atomic::{self, AtomicUsize}; -use std::sync::{Arc, Condvar, Mutex}; -use std::time::{Duration, Instant}; +use std::{ + cmp::{max, min}, + fs, + io::{self, Read, Seek, SeekFrom}, + sync::{ + atomic::{self, AtomicUsize}, + Arc, Condvar, Mutex, + }, + time::{Duration, Instant}, +}; -use futures_util::future::IntoStream; -use futures_util::{StreamExt, TryFutureExt}; -use hyper::client::ResponseFuture; -use hyper::header::CONTENT_RANGE; -use hyper::Body; +use futures_util::{future::IntoStream, StreamExt, TryFutureExt}; +use hyper::{client::ResponseFuture, header::CONTENT_RANGE, Body, Response, StatusCode}; use tempfile::NamedTempFile; use thiserror::Error; use tokio::sync::{mpsc, oneshot}; -use librespot_core::cdn_url::{CdnUrl, CdnUrlError}; -use librespot_core::file_id::FileId; -use librespot_core::session::Session; -use librespot_core::spclient::SpClientError; +use librespot_core::{cdn_url::CdnUrl, Error, FileId, Session}; use self::receive::audio_file_fetch; use crate::range_set::{Range, RangeSet}; -pub type AudioFileResult = Result<(), AudioFileError>; +pub type AudioFileResult = Result<(), librespot_core::Error>; #[derive(Error, Debug)] pub enum AudioFileError { - #[error("could not complete CDN request: {0}")] - Cdn(#[from] hyper::Error), - #[error("channel was disconnected")] + #[error("other end of channel disconnected")] Channel, - #[error("empty response")] - Empty, - #[error("I/O error: {0}")] - Io(#[from] io::Error), - #[error("output file unavailable")] + #[error("required header not found")] + Header, + #[error("streamer received no data")] + NoData, + #[error("no output available")] Output, - #[error("error parsing response")] - Parsing, - #[error("mutex was poisoned")] - Poisoned, - #[error("could not complete API request: {0}")] - SpClient(#[from] SpClientError), - #[error("streamer did not report progress")] - Timeout, - #[error("could not get CDN URL: {0}")] - Url(#[from] CdnUrlError), + #[error("invalid status code {0}")] + StatusCode(StatusCode), + #[error("wait timeout exceeded")] + WaitTimeout, +} + +impl From for Error { + fn from(err: AudioFileError) -> Self { + match err { + AudioFileError::Channel => Error::aborted(err), + AudioFileError::Header => Error::unavailable(err), + AudioFileError::NoData => Error::unavailable(err), + AudioFileError::Output => Error::aborted(err), + AudioFileError::StatusCode(_) => Error::failed_precondition(err), + AudioFileError::WaitTimeout => Error::deadline_exceeded(err), + } + } } /// The minimum size of a block that is requested from the Spotify servers in one request. @@ -124,7 +127,7 @@ pub enum AudioFile { #[derive(Debug)] pub struct StreamingRequest { streamer: IntoStream, - initial_body: Option, + initial_response: Option>, offset: usize, length: usize, request_time: Instant, @@ -154,12 +157,9 @@ impl StreamLoaderController { self.file_size == 0 } - pub fn range_available(&self, range: Range) -> Result { + pub fn range_available(&self, range: Range) -> bool { let available = if let Some(ref shared) = self.stream_shared { - let download_status = shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let download_status = shared.download_status.lock().unwrap(); range.length <= download_status @@ -169,16 +169,16 @@ impl StreamLoaderController { range.length <= self.len() - range.start }; - Ok(available) + available } - pub fn range_to_end_available(&self) -> Result { + pub fn range_to_end_available(&self) -> bool { match self.stream_shared { Some(ref shared) => { let read_position = shared.read_position.load(atomic::Ordering::Relaxed); self.range_available(Range::new(read_position, self.len() - read_position)) } - None => Ok(true), + None => true, } } @@ -190,7 +190,8 @@ impl StreamLoaderController { fn send_stream_loader_command(&self, command: StreamLoaderCommand) { if let Some(ref channel) = self.channel_tx { - // ignore the error in case the channel has been closed already. + // Ignore the error in case the channel has been closed already. + // This means that the file was completely downloaded. let _ = channel.send(command); } } @@ -213,10 +214,7 @@ impl StreamLoaderController { self.fetch(range); if let Some(ref shared) = self.stream_shared { - let mut download_status = shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let mut download_status = shared.download_status.lock().unwrap(); while range.length > download_status @@ -226,7 +224,7 @@ impl StreamLoaderController { download_status = shared .cond .wait_timeout(download_status, DOWNLOAD_TIMEOUT) - .map_err(|_| AudioFileError::Timeout)? + .map_err(|_| AudioFileError::WaitTimeout)? .0; if range.length > (download_status @@ -319,7 +317,7 @@ impl AudioFile { file_id: FileId, bytes_per_second: usize, play_from_beginning: bool, - ) -> Result { + ) -> Result { if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) { debug!("File {} already in cache", file_id); return Ok(AudioFile::Cached(file)); @@ -340,9 +338,14 @@ impl AudioFile { let session_ = session.clone(); session.spawn(complete_rx.map_ok(move |mut file| { if let Some(cache) = session_.cache() { - if cache.save_file(file_id, &mut file) { - debug!("File {} cached to {:?}", file_id, cache.file(file_id)); + if let Some(cache_id) = cache.file(file_id) { + if let Err(e) = cache.save_file(file_id, &mut file) { + error!("Error caching file {} to {:?}: {}", file_id, cache_id, e); + } else { + debug!("File {} cached to {:?}", file_id, cache_id); + } } + debug!("Downloading file {} complete", file_id); } })); @@ -350,7 +353,7 @@ impl AudioFile { Ok(AudioFile::Streaming(streaming.await?)) } - pub fn get_stream_loader_controller(&self) -> Result { + pub fn get_stream_loader_controller(&self) -> Result { let controller = match self { AudioFile::Streaming(ref stream) => StreamLoaderController { channel_tx: Some(stream.stream_loader_command_tx.clone()), @@ -379,7 +382,7 @@ impl AudioFileStreaming { complete_tx: oneshot::Sender, bytes_per_second: usize, play_from_beginning: bool, - ) -> Result { + ) -> Result { let download_size = if play_from_beginning { INITIAL_DOWNLOAD_SIZE + max( @@ -392,8 +395,8 @@ impl AudioFileStreaming { INITIAL_DOWNLOAD_SIZE }; - let mut cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?; - let url = cdn_url.get_url()?; + let cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?; + let url = cdn_url.try_get_url()?; trace!("Streaming {:?}", url); @@ -403,23 +406,19 @@ impl AudioFileStreaming { // Get the first chunk with the headers to get the file size. // The remainder of that chunk with possibly also a response body is then // further processed in `audio_file_fetch`. - let response = match streamer.next().await { - Some(Ok(data)) => data, - Some(Err(e)) => return Err(AudioFileError::Cdn(e)), - None => return Err(AudioFileError::Empty), - }; + let response = streamer.next().await.ok_or(AudioFileError::NoData)??; + let header_value = response .headers() .get(CONTENT_RANGE) - .ok_or(AudioFileError::Parsing)?; - - let str_value = header_value.to_str().map_err(|_| AudioFileError::Parsing)?; - let file_size_str = str_value.split('/').last().ok_or(AudioFileError::Parsing)?; - let file_size = file_size_str.parse().map_err(|_| AudioFileError::Parsing)?; + .ok_or(AudioFileError::Header)?; + let str_value = header_value.to_str()?; + let file_size_str = str_value.split('/').last().unwrap_or_default(); + let file_size = file_size_str.parse()?; let initial_request = StreamingRequest { streamer, - initial_body: Some(response.into_body()), + initial_response: Some(response), offset: 0, length: download_size, request_time, @@ -474,12 +473,7 @@ impl Read for AudioFileStreaming { let length = min(output.len(), self.shared.file_size - offset); - let length_to_request = match *(self - .shared - .download_strategy - .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?) - { + let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) { DownloadStrategy::RandomAccess() => length, DownloadStrategy::Streaming() => { // Due to the read-ahead stuff, we potentially request more than the actual request demanded. @@ -503,42 +497,32 @@ impl Read for AudioFileStreaming { let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length_to_request)); - let mut download_status = self - .shared - .download_status - .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))?; + let mut download_status = self.shared.download_status.lock().unwrap(); + ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); for &range in ranges_to_request.iter() { self.stream_loader_command_tx .send(StreamLoaderCommand::Fetch(range)) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "tx channel is disconnected"))?; + .map_err(|err| io::Error::new(io::ErrorKind::BrokenPipe, err))?; } if length == 0 { return Ok(0); } - let mut download_message_printed = false; while !download_status.downloaded.contains(offset) { - if let DownloadStrategy::Streaming() = *self - .shared - .download_strategy - .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex was poisoned"))? - { - if !download_message_printed { - debug!("Stream waiting for download of file position {}. Downloaded ranges: {}. Pending ranges: {}", offset, download_status.downloaded, download_status.requested.minus(&download_status.downloaded)); - download_message_printed = true; - } - } download_status = self .shared .cond .wait_timeout(download_status, DOWNLOAD_TIMEOUT) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "timeout acquiring mutex"))? + .map_err(|_| { + io::Error::new( + io::ErrorKind::TimedOut, + Error::deadline_exceeded(AudioFileError::WaitTimeout), + ) + })? .0; } let available_length = download_status @@ -551,15 +535,6 @@ impl Read for AudioFileStreaming { let read_len = min(length, available_length); let read_len = self.read_file.read(&mut output[..read_len])?; - if download_message_printed { - debug!( - "Read at postion {} completed. {} bytes returned, {} bytes were requested.", - offset, - read_len, - output.len() - ); - } - self.position += read_len as u64; self.shared .read_position diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 716c24e1..f26c95f8 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -1,25 +1,25 @@ -use std::cmp::{max, min}; -use std::io::{Seek, SeekFrom, Write}; -use std::sync::{atomic, Arc}; -use std::time::{Duration, Instant}; +use std::{ + cmp::{max, min}, + io::{Seek, SeekFrom, Write}, + sync::{atomic, Arc}, + time::{Duration, Instant}, +}; use atomic::Ordering; use bytes::Bytes; use futures_util::StreamExt; +use hyper::StatusCode; use tempfile::NamedTempFile; use tokio::sync::{mpsc, oneshot}; -use librespot_core::session::Session; +use librespot_core::{session::Session, Error}; use crate::range_set::{Range, RangeSet}; use super::{ AudioFileError, AudioFileResult, AudioFileShared, DownloadStrategy, StreamLoaderCommand, - StreamingRequest, -}; -use super::{ - FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, - MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, + StreamingRequest, FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, + MAX_PREFETCH_REQUESTS, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, }; struct PartialFileData { @@ -49,19 +49,27 @@ async fn receive_data( let mut measure_ping_time = old_number_of_request == 0; - let result = loop { - let body = match request.initial_body.take() { + let result: Result<_, Error> = loop { + let response = match request.initial_response.take() { Some(data) => data, None => match request.streamer.next().await { - Some(Ok(response)) => response.into_body(), - Some(Err(e)) => break Err(e), + Some(Ok(response)) => response, + Some(Err(e)) => break Err(e.into()), None => break Ok(()), }, }; + let code = response.status(); + let body = response.into_body(); + + if code != StatusCode::PARTIAL_CONTENT { + debug!("Streamer expected partial content but got: {}", code); + break Err(AudioFileError::StatusCode(code).into()); + } + let data = match hyper::body::to_bytes(body).await { Ok(bytes) => bytes, - Err(e) => break Err(e), + Err(e) => break Err(e.into()), }; if measure_ping_time { @@ -69,16 +77,16 @@ async fn receive_data( if duration > MAXIMUM_ASSUMED_PING_TIME { duration = MAXIMUM_ASSUMED_PING_TIME; } - let _ = file_data_tx.send(ReceivedData::ResponseTime(duration)); + file_data_tx.send(ReceivedData::ResponseTime(duration))?; measure_ping_time = false; } let data_size = data.len(); - let _ = file_data_tx.send(ReceivedData::Data(PartialFileData { + file_data_tx.send(ReceivedData::Data(PartialFileData { offset: data_offset, data, - })); + }))?; data_offset += data_size; if request_length < data_size { warn!( @@ -100,10 +108,8 @@ async fn receive_data( if request_length > 0 { let missing_range = Range::new(data_offset, request_length); - let mut download_status = shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let mut download_status = shared.download_status.lock().unwrap(); + download_status.requested.subtract_range(&missing_range); shared.cond.notify_all(); } @@ -127,7 +133,7 @@ async fn receive_data( "Error from streamer for range {} (+{}): {:?}", requested_offset, requested_length, e ); - Err(e.into()) + Err(e) } } } @@ -150,14 +156,8 @@ enum ControlFlow { } impl AudioFileFetch { - fn get_download_strategy(&mut self) -> Result { - let strategy = self - .shared - .download_strategy - .lock() - .map_err(|_| AudioFileError::Poisoned)?; - - Ok(*(strategy)) + fn get_download_strategy(&mut self) -> DownloadStrategy { + *(self.shared.download_strategy.lock().unwrap()) } fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult { @@ -172,52 +172,34 @@ impl AudioFileFetch { let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length)); - let mut download_status = self - .shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let mut download_status = self.shared.download_status.lock().unwrap(); ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); - let cdn_url = &self.shared.cdn_url; - let file_id = cdn_url.file_id; - for range in ranges_to_request.iter() { - match cdn_url.urls.first() { - Some(url) => { - match self - .session - .spclient() - .stream_file(&url.0, range.start, range.length) - { - Ok(streamer) => { - download_status.requested.add_range(range); + let url = self.shared.cdn_url.try_get_url()?; - let streaming_request = StreamingRequest { - streamer, - initial_body: None, - offset: range.start, - length: range.length, - request_time: Instant::now(), - }; + let streamer = self + .session + .spclient() + .stream_file(url, range.start, range.length)?; - self.session.spawn(receive_data( - self.shared.clone(), - self.file_data_tx.clone(), - streaming_request, - )); - } - Err(e) => { - error!("Unable to open stream for track <{}>: {:?}", file_id, e); - } - } - } - None => { - error!("Unable to get CDN URL for track <{}>", file_id); - } - } + download_status.requested.add_range(range); + + let streaming_request = StreamingRequest { + streamer, + initial_response: None, + offset: range.start, + length: range.length, + request_time: Instant::now(), + }; + + self.session.spawn(receive_data( + self.shared.clone(), + self.file_data_tx.clone(), + streaming_request, + )); } Ok(()) @@ -236,11 +218,8 @@ impl AudioFileFetch { let mut missing_data = RangeSet::new(); missing_data.add_range(&Range::new(0, self.shared.file_size)); { - let download_status = self - .shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let download_status = self.shared.download_status.lock().unwrap(); + missing_data.subtract_range_set(&download_status.downloaded); missing_data.subtract_range_set(&download_status.requested); } @@ -277,7 +256,7 @@ impl AudioFileFetch { Ok(()) } - fn handle_file_data(&mut self, data: ReceivedData) -> Result { + fn handle_file_data(&mut self, data: ReceivedData) -> Result { match data { ReceivedData::ResponseTime(response_time) => { let old_ping_time_ms = self.shared.ping_time_ms.load(Ordering::Relaxed); @@ -324,14 +303,10 @@ impl AudioFileFetch { output.seek(SeekFrom::Start(data.offset as u64))?; output.write_all(data.data.as_ref())?; } - None => return Err(AudioFileError::Output), + None => return Err(AudioFileError::Output.into()), } - let mut download_status = self - .shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let mut download_status = self.shared.download_status.lock().unwrap(); let received_range = Range::new(data.offset, data.data.len()); download_status.downloaded.add_range(&received_range); @@ -355,38 +330,38 @@ impl AudioFileFetch { fn handle_stream_loader_command( &mut self, cmd: StreamLoaderCommand, - ) -> Result { + ) -> Result { match cmd { StreamLoaderCommand::Fetch(request) => { self.download_range(request.start, request.length)?; } StreamLoaderCommand::RandomAccessMode() => { - *(self - .shared - .download_strategy - .lock() - .map_err(|_| AudioFileError::Poisoned)?) = DownloadStrategy::RandomAccess(); + *(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::RandomAccess(); } StreamLoaderCommand::StreamMode() => { - *(self - .shared - .download_strategy - .lock() - .map_err(|_| AudioFileError::Poisoned)?) = DownloadStrategy::Streaming(); + *(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::Streaming(); } StreamLoaderCommand::Close() => return Ok(ControlFlow::Break), } + Ok(ControlFlow::Continue) } fn finish(&mut self) -> AudioFileResult { - let mut output = self.output.take().ok_or(AudioFileError::Output)?; - let complete_tx = self.complete_tx.take().ok_or(AudioFileError::Output)?; + let output = self.output.take(); - output.seek(SeekFrom::Start(0))?; - complete_tx - .send(output) - .map_err(|_| AudioFileError::Channel) + let complete_tx = self.complete_tx.take(); + + if let Some(mut output) = output { + output.seek(SeekFrom::Start(0))?; + if let Some(complete_tx) = complete_tx { + complete_tx + .send(output) + .map_err(|_| AudioFileError::Channel)?; + } + } + + Ok(()) } } @@ -405,10 +380,8 @@ pub(super) async fn audio_file_fetch( initial_request.offset, initial_request.offset + initial_request.length, ); - let mut download_status = shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let mut download_status = shared.download_status.lock().unwrap(); + download_status.requested.add_range(&requested_range); } @@ -452,18 +425,15 @@ pub(super) async fn audio_file_fetch( } } - if fetch.get_download_strategy()? == DownloadStrategy::Streaming() { + if fetch.get_download_strategy() == DownloadStrategy::Streaming() { let number_of_open_requests = fetch.shared.number_of_open_requests.load(Ordering::SeqCst); if number_of_open_requests < MAX_PREFETCH_REQUESTS { let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests; let bytes_pending: usize = { - let download_status = fetch - .shared - .download_status - .lock() - .map_err(|_| AudioFileError::Poisoned)?; + let download_status = fetch.shared.download_status.lock().unwrap(); + download_status .requested .minus(&download_status.downloaded) diff --git a/audio/src/range_set.rs b/audio/src/range_set.rs index a37b03ae..005a4cda 100644 --- a/audio/src/range_set.rs +++ b/audio/src/range_set.rs @@ -1,6 +1,8 @@ -use std::cmp::{max, min}; -use std::fmt; -use std::slice::Iter; +use std::{ + cmp::{max, min}, + fmt, + slice::Iter, +}; #[derive(Copy, Clone, Debug)] pub struct Range { diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 4daf89f4..b0878c1c 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -15,6 +15,7 @@ protobuf = "2.14.0" rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +thiserror = "1.0" tokio = { version = "1.0", features = ["macros", "sync"] } tokio-stream = "0.1.1" diff --git a/connect/src/context.rs b/connect/src/context.rs index 154d9507..928aec23 100644 --- a/connect/src/context.rs +++ b/connect/src/context.rs @@ -1,7 +1,12 @@ +// TODO : move to metadata + use crate::core::spotify_id::SpotifyId; use crate::protocol::spirc::TrackRef; -use serde::Deserialize; +use serde::{ + de::{Error, Unexpected}, + Deserialize, +}; #[derive(Deserialize, Debug)] pub struct StationContext { @@ -72,17 +77,23 @@ where D: serde::Deserializer<'d>, { let v: Vec = serde::Deserialize::deserialize(de)?; - let track_vec = v - .iter() + v.iter() .map(|v| { let mut t = TrackRef::new(); // This has got to be the most round about way of doing this. - t.set_gid(SpotifyId::from_base62(&v.gid).unwrap().to_raw().to_vec()); + t.set_gid( + SpotifyId::from_base62(&v.gid) + .map_err(|_| { + D::Error::invalid_value( + Unexpected::Str(&v.gid), + &"a Base-62 encoded Spotify ID", + ) + })? + .to_raw() + .to_vec(), + ); t.set_uri(v.uri.to_owned()); - - t + Ok(t) }) - .collect::>(); - - Ok(track_vec) + .collect::, D::Error>>() } diff --git a/connect/src/discovery.rs b/connect/src/discovery.rs index 8ce3f4f0..8f4f9b34 100644 --- a/connect/src/discovery.rs +++ b/connect/src/discovery.rs @@ -1,10 +1,11 @@ -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; use futures_util::Stream; -use librespot_core::authentication::Credentials; -use librespot_core::config::ConnectConfig; +use librespot_core::{authentication::Credentials, config::ConnectConfig}; pub struct DiscoveryStream(librespot_discovery::Discovery); diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index b3878a42..dc631831 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1,31 +1,67 @@ -use std::convert::TryFrom; -use std::future::Future; -use std::pin::Pin; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + convert::TryFrom, + future::Future, + pin::Pin, + time::{SystemTime, UNIX_EPOCH}, +}; -use crate::context::StationContext; -use crate::core::config::ConnectConfig; -use crate::core::mercury::{MercuryError, MercurySender}; -use crate::core::session::{Session, UserAttributes}; -use crate::core::spotify_id::SpotifyId; -use crate::core::util::SeqGenerator; -use crate::core::version; -use crate::playback::mixer::Mixer; -use crate::playback::player::{Player, PlayerEvent, PlayerEventChannel}; +use futures_util::{ + future::{self, FusedFuture}, + stream::FusedStream, + FutureExt, StreamExt, TryFutureExt, +}; -use crate::protocol; -use crate::protocol::explicit_content_pubsub::UserAttributesUpdate; -use crate::protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef}; -use crate::protocol::user_attributes::UserAttributesMutation; - -use futures_util::future::{self, FusedFuture}; -use futures_util::stream::FusedStream; -use futures_util::{FutureExt, StreamExt}; use protobuf::{self, Message}; use rand::seq::SliceRandom; +use thiserror::Error; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; +use crate::{ + context::StationContext, + core::{ + config::ConnectConfig, // TODO: move to connect? + mercury::{MercuryError, MercurySender}, + session::UserAttributes, + util::SeqGenerator, + version, + Error, + Session, + SpotifyId, + }, + playback::{ + mixer::Mixer, + player::{Player, PlayerEvent, PlayerEventChannel}, + }, + protocol::{ + self, + explicit_content_pubsub::UserAttributesUpdate, + spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef}, + user_attributes::UserAttributesMutation, + }, +}; + +#[derive(Debug, Error)] +pub enum SpircError { + #[error("response payload empty")] + NoData, + #[error("message addressed at another ident: {0}")] + Ident(String), + #[error("message pushed for another URI")] + InvalidUri(String), +} + +impl From for Error { + fn from(err: SpircError) -> Self { + match err { + SpircError::NoData => Error::unavailable(err), + SpircError::Ident(_) => Error::aborted(err), + SpircError::InvalidUri(_) => Error::aborted(err), + } + } +} + +#[derive(Debug)] enum SpircPlayStatus { Stopped, LoadingPlay { @@ -60,18 +96,18 @@ struct SpircTask { play_request_id: Option, play_status: SpircPlayStatus, - subscription: BoxedStream, - connection_id_update: BoxedStream, - user_attributes_update: BoxedStream, - user_attributes_mutation: BoxedStream, + remote_update: BoxedStream>, + connection_id_update: BoxedStream>, + user_attributes_update: BoxedStream>, + user_attributes_mutation: BoxedStream>, sender: MercurySender, commands: Option>, player_events: Option, shutdown: bool, session: Session, - context_fut: BoxedFuture>, - autoplay_fut: BoxedFuture>, + context_fut: BoxedFuture>, + autoplay_fut: BoxedFuture>, context: Option, } @@ -232,7 +268,7 @@ impl Spirc { session: Session, player: Player, mixer: Box, - ) -> (Spirc, impl Future) { + ) -> Result<(Spirc, impl Future), Error> { debug!("new Spirc[{}]", session.session_id()); let ident = session.device_id().to_owned(); @@ -242,16 +278,18 @@ impl Spirc { debug!("canonical_username: {}", canonical_username); let uri = format!("hm://remote/user/{}/", url_encode(canonical_username)); - let subscription = Box::pin( + let remote_update = Box::pin( session .mercury() .subscribe(uri.clone()) - .map(Result::unwrap) + .inspect_err(|x| error!("remote update error: {}", x)) + .and_then(|x| async move { Ok(x) }) + .map(Result::unwrap) // guaranteed to be safe by `and_then` above .map(UnboundedReceiverStream::new) .flatten_stream() - .map(|response| -> Frame { - let data = response.payload.first().unwrap(); - Frame::parse_from_bytes(data).unwrap() + .map(|response| -> Result { + let data = response.payload.first().ok_or(SpircError::NoData)?; + Ok(Frame::parse_from_bytes(data)?) }), ); @@ -261,12 +299,12 @@ impl Spirc { .listen_for("hm://pusher/v1/connections/") .map(UnboundedReceiverStream::new) .flatten_stream() - .map(|response| -> String { - response + .map(|response| -> Result { + let connection_id = response .uri .strip_prefix("hm://pusher/v1/connections/") - .unwrap_or("") - .to_owned() + .ok_or_else(|| SpircError::InvalidUri(response.uri.clone()))?; + Ok(connection_id.to_owned()) }), ); @@ -276,9 +314,9 @@ impl Spirc { .listen_for("spotify:user:attributes:update") .map(UnboundedReceiverStream::new) .flatten_stream() - .map(|response| -> UserAttributesUpdate { - let data = response.payload.first().unwrap(); - UserAttributesUpdate::parse_from_bytes(data).unwrap() + .map(|response| -> Result { + let data = response.payload.first().ok_or(SpircError::NoData)?; + Ok(UserAttributesUpdate::parse_from_bytes(data)?) }), ); @@ -288,9 +326,9 @@ impl Spirc { .listen_for("spotify:user:attributes:mutated") .map(UnboundedReceiverStream::new) .flatten_stream() - .map(|response| -> UserAttributesMutation { - let data = response.payload.first().unwrap(); - UserAttributesMutation::parse_from_bytes(data).unwrap() + .map(|response| -> Result { + let data = response.payload.first().ok_or(SpircError::NoData)?; + Ok(UserAttributesMutation::parse_from_bytes(data)?) }), ); @@ -321,7 +359,7 @@ impl Spirc { play_request_id: None, play_status: SpircPlayStatus::Stopped, - subscription, + remote_update, connection_id_update, user_attributes_update, user_attributes_mutation, @@ -346,37 +384,37 @@ impl Spirc { let spirc = Spirc { commands: cmd_tx }; - task.hello(); + task.hello()?; - (spirc, task.run()) + Ok((spirc, task.run())) } - pub fn play(&self) { - let _ = self.commands.send(SpircCommand::Play); + pub fn play(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Play)?) } - pub fn play_pause(&self) { - let _ = self.commands.send(SpircCommand::PlayPause); + pub fn play_pause(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::PlayPause)?) } - pub fn pause(&self) { - let _ = self.commands.send(SpircCommand::Pause); + pub fn pause(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Pause)?) } - pub fn prev(&self) { - let _ = self.commands.send(SpircCommand::Prev); + pub fn prev(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Prev)?) } - pub fn next(&self) { - let _ = self.commands.send(SpircCommand::Next); + pub fn next(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Next)?) } - pub fn volume_up(&self) { - let _ = self.commands.send(SpircCommand::VolumeUp); + pub fn volume_up(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::VolumeUp)?) } - pub fn volume_down(&self) { - let _ = self.commands.send(SpircCommand::VolumeDown); + pub fn volume_down(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::VolumeDown)?) } - pub fn shutdown(&self) { - let _ = self.commands.send(SpircCommand::Shutdown); + pub fn shutdown(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Shutdown)?) } - pub fn shuffle(&self) { - let _ = self.commands.send(SpircCommand::Shuffle); + pub fn shuffle(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Shuffle)?) } } @@ -386,39 +424,57 @@ impl SpircTask { let commands = self.commands.as_mut(); let player_events = self.player_events.as_mut(); tokio::select! { - frame = self.subscription.next() => match frame { - Some(frame) => self.handle_frame(frame), + remote_update = self.remote_update.next() => match remote_update { + Some(result) => match result { + Ok(update) => if let Err(e) = self.handle_remote_update(update) { + error!("could not dispatch remote update: {}", e); + } + Err(e) => error!("could not parse remote update: {}", e), + } None => { error!("subscription terminated"); break; } }, user_attributes_update = self.user_attributes_update.next() => match user_attributes_update { - Some(attributes) => self.handle_user_attributes_update(attributes), + Some(result) => match result { + Ok(attributes) => self.handle_user_attributes_update(attributes), + Err(e) => error!("could not parse user attributes update: {}", e), + } None => { error!("user attributes update selected, but none received"); break; } }, user_attributes_mutation = self.user_attributes_mutation.next() => match user_attributes_mutation { - Some(attributes) => self.handle_user_attributes_mutation(attributes), + Some(result) => match result { + Ok(attributes) => self.handle_user_attributes_mutation(attributes), + Err(e) => error!("could not parse user attributes mutation: {}", e), + } None => { error!("user attributes mutation selected, but none received"); break; } }, connection_id_update = self.connection_id_update.next() => match connection_id_update { - Some(connection_id) => self.handle_connection_id_update(connection_id), + Some(result) => match result { + Ok(connection_id) => self.handle_connection_id_update(connection_id), + Err(e) => error!("could not parse connection ID update: {}", e), + } None => { error!("connection ID update selected, but none received"); break; } }, - cmd = async { commands.unwrap().recv().await }, if commands.is_some() => if let Some(cmd) = cmd { - self.handle_command(cmd); + cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = cmd { + if let Err(e) = self.handle_command(cmd) { + error!("could not dispatch command: {}", e); + } }, - event = async { player_events.unwrap().recv().await }, if player_events.is_some() => if let Some(event) = event { - self.handle_player_event(event) + event = async { player_events?.recv().await }, if player_events.is_some() => if let Some(event) = event { + if let Err(e) = self.handle_player_event(event) { + error!("could not dispatch player event: {}", e); + } }, result = self.sender.flush(), if !self.sender.is_flushed() => if result.is_err() { error!("Cannot flush spirc event sender."); @@ -488,79 +544,80 @@ impl SpircTask { self.state.set_position_ms(position_ms); } - fn handle_command(&mut self, cmd: SpircCommand) { + fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> { let active = self.device.get_is_active(); match cmd { SpircCommand::Play => { if active { self.handle_play(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypePlay).send(); + CommandSender::new(self, MessageType::kMessageTypePlay).send() } } SpircCommand::PlayPause => { if active { self.handle_play_pause(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypePlayPause).send(); + CommandSender::new(self, MessageType::kMessageTypePlayPause).send() } } SpircCommand::Pause => { if active { self.handle_pause(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypePause).send(); + CommandSender::new(self, MessageType::kMessageTypePause).send() } } SpircCommand::Prev => { if active { self.handle_prev(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypePrev).send(); + CommandSender::new(self, MessageType::kMessageTypePrev).send() } } SpircCommand::Next => { if active { self.handle_next(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypeNext).send(); + CommandSender::new(self, MessageType::kMessageTypeNext).send() } } SpircCommand::VolumeUp => { if active { self.handle_volume_up(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send(); + CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send() } } SpircCommand::VolumeDown => { if active { self.handle_volume_down(); - self.notify(None, true); + self.notify(None, true) } else { - CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send(); + CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send() } } SpircCommand::Shutdown => { - CommandSender::new(self, MessageType::kMessageTypeGoodbye).send(); + CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?; self.shutdown = true; if let Some(rx) = self.commands.as_mut() { rx.close() } + Ok(()) } SpircCommand::Shuffle => { - CommandSender::new(self, MessageType::kMessageTypeShuffle).send(); + CommandSender::new(self, MessageType::kMessageTypeShuffle).send() } } } - fn handle_player_event(&mut self, event: PlayerEvent) { + fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> { // we only process events if the play_request_id matches. If it doesn't, it is // an event that belongs to a previous track and only arrives now due to a race // condition. In this case we have updated the state already and don't want to @@ -571,6 +628,7 @@ impl SpircTask { PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(), PlayerEvent::Loading { .. } => self.notify(None, false), PlayerEvent::Playing { position_ms, .. } => { + trace!("==> kPlayStatusPlay"); let new_nominal_start_time = self.now_ms() - position_ms as i64; match self.play_status { SpircPlayStatus::Playing { @@ -580,27 +638,29 @@ impl SpircTask { if (*nominal_start_time - new_nominal_start_time).abs() > 100 { *nominal_start_time = new_nominal_start_time; self.update_state_position(position_ms); - self.notify(None, true); + self.notify(None, true) + } else { + Ok(()) } } SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => { self.state.set_status(PlayStatus::kPlayStatusPlay); self.update_state_position(position_ms); - self.notify(None, true); self.play_status = SpircPlayStatus::Playing { nominal_start_time: new_nominal_start_time, preloading_of_next_track_triggered: false, }; + self.notify(None, true) } - _ => (), - }; - trace!("==> kPlayStatusPlay"); + _ => Ok(()), + } } PlayerEvent::Paused { position_ms: new_position_ms, .. } => { + trace!("==> kPlayStatusPause"); match self.play_status { SpircPlayStatus::Paused { ref mut position_ms, @@ -609,37 +669,48 @@ impl SpircTask { if *position_ms != new_position_ms { *position_ms = new_position_ms; self.update_state_position(new_position_ms); - self.notify(None, true); + self.notify(None, true) + } else { + Ok(()) } } SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => { self.state.set_status(PlayStatus::kPlayStatusPause); self.update_state_position(new_position_ms); - self.notify(None, true); self.play_status = SpircPlayStatus::Paused { position_ms: new_position_ms, preloading_of_next_track_triggered: false, }; + self.notify(None, true) } - _ => (), + _ => Ok(()), } - trace!("==> kPlayStatusPause"); } PlayerEvent::Stopped { .. } => match self.play_status { - SpircPlayStatus::Stopped => (), + SpircPlayStatus::Stopped => Ok(()), _ => { warn!("The player has stopped unexpectedly."); self.state.set_status(PlayStatus::kPlayStatusStop); - self.notify(None, true); self.play_status = SpircPlayStatus::Stopped; + self.notify(None, true) } }, - PlayerEvent::TimeToPreloadNextTrack { .. } => self.handle_preload_next_track(), - PlayerEvent::Unavailable { track_id, .. } => self.handle_unavailable(track_id), - _ => (), + PlayerEvent::TimeToPreloadNextTrack { .. } => { + self.handle_preload_next_track(); + Ok(()) + } + PlayerEvent::Unavailable { track_id, .. } => { + self.handle_unavailable(track_id); + Ok(()) + } + _ => Ok(()), } + } else { + Ok(()) } + } else { + Ok(()) } } @@ -655,7 +726,7 @@ impl SpircTask { .iter() .map(|pair| (pair.get_key().to_owned(), pair.get_value().to_owned())) .collect(); - let _ = self.session.set_user_attributes(attributes); + self.session.set_user_attributes(attributes) } fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) { @@ -683,8 +754,8 @@ impl SpircTask { } } - fn handle_frame(&mut self, frame: Frame) { - let state_string = match frame.get_state().get_status() { + fn handle_remote_update(&mut self, update: Frame) -> Result<(), Error> { + let state_string = match update.get_state().get_status() { PlayStatus::kPlayStatusLoading => "kPlayStatusLoading", PlayStatus::kPlayStatusPause => "kPlayStatusPause", PlayStatus::kPlayStatusStop => "kPlayStatusStop", @@ -693,24 +764,24 @@ impl SpircTask { debug!( "{:?} {:?} {} {} {} {}", - frame.get_typ(), - frame.get_device_state().get_name(), - frame.get_ident(), - frame.get_seq_nr(), - frame.get_state_update_id(), + update.get_typ(), + update.get_device_state().get_name(), + update.get_ident(), + update.get_seq_nr(), + update.get_state_update_id(), state_string, ); - if frame.get_ident() == self.ident - || (!frame.get_recipient().is_empty() && !frame.get_recipient().contains(&self.ident)) + let device_id = &self.ident; + let ident = update.get_ident(); + if ident == device_id + || (!update.get_recipient().is_empty() && !update.get_recipient().contains(device_id)) { - return; + return Err(SpircError::Ident(ident.to_string()).into()); } - match frame.get_typ() { - MessageType::kMessageTypeHello => { - self.notify(Some(frame.get_ident()), true); - } + match update.get_typ() { + MessageType::kMessageTypeHello => self.notify(Some(ident), true), MessageType::kMessageTypeLoad => { if !self.device.get_is_active() { @@ -719,12 +790,12 @@ impl SpircTask { self.device.set_became_active_at(now); } - self.update_tracks(&frame); + self.update_tracks(&update); if !self.state.get_track().is_empty() { let start_playing = - frame.get_state().get_status() == PlayStatus::kPlayStatusPlay; - self.load_track(start_playing, frame.get_state().get_position_ms()); + update.get_state().get_status() == PlayStatus::kPlayStatusPlay; + self.load_track(start_playing, update.get_state().get_position_ms()); } else { info!("No more tracks left in queue"); self.state.set_status(PlayStatus::kPlayStatusStop); @@ -732,51 +803,51 @@ impl SpircTask { self.play_status = SpircPlayStatus::Stopped; } - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypePlay => { self.handle_play(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypePlayPause => { self.handle_play_pause(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypePause => { self.handle_pause(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypeNext => { self.handle_next(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypePrev => { self.handle_prev(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypeVolumeUp => { self.handle_volume_up(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypeVolumeDown => { self.handle_volume_down(); - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypeRepeat => { - self.state.set_repeat(frame.get_state().get_repeat()); - self.notify(None, true); + self.state.set_repeat(update.get_state().get_repeat()); + self.notify(None, true) } MessageType::kMessageTypeShuffle => { - self.state.set_shuffle(frame.get_state().get_shuffle()); + self.state.set_shuffle(update.get_state().get_shuffle()); if self.state.get_shuffle() { let current_index = self.state.get_playing_track_index(); { @@ -792,17 +863,17 @@ impl SpircTask { let context = self.state.get_context_uri(); debug!("{:?}", context); } - self.notify(None, true); + self.notify(None, true) } MessageType::kMessageTypeSeek => { - self.handle_seek(frame.get_position()); - self.notify(None, true); + self.handle_seek(update.get_position()); + self.notify(None, true) } MessageType::kMessageTypeReplace => { - self.update_tracks(&frame); - self.notify(None, true); + self.update_tracks(&update); + self.notify(None, true)?; if let SpircPlayStatus::Playing { preloading_of_next_track_triggered, @@ -820,27 +891,29 @@ impl SpircTask { } } } + Ok(()) } MessageType::kMessageTypeVolume => { - self.set_volume(frame.get_volume() as u16); - self.notify(None, true); + self.set_volume(update.get_volume() as u16); + self.notify(None, true) } MessageType::kMessageTypeNotify => { if self.device.get_is_active() - && frame.get_device_state().get_is_active() + && update.get_device_state().get_is_active() && self.device.get_became_active_at() - <= frame.get_device_state().get_became_active_at() + <= update.get_device_state().get_became_active_at() { self.device.set_is_active(false); self.state.set_status(PlayStatus::kPlayStatusStop); self.player.stop(); self.play_status = SpircPlayStatus::Stopped; } + Ok(()) } - _ => (), + _ => Ok(()), } } @@ -850,6 +923,7 @@ impl SpircTask { position_ms, preloading_of_next_track_triggered, } => { + // TODO - also apply this to the arm below // Synchronize the volume from the mixer. This is useful on // systems that can switch sources from and back to librespot. let current_volume = self.mixer.volume(); @@ -864,6 +938,8 @@ impl SpircTask { }; } SpircPlayStatus::LoadingPause { position_ms } => { + // TODO - fix "Player::play called from invalid state" when hitting play + // on initial start-up, when starting halfway a track self.player.play(); self.play_status = SpircPlayStatus::LoadingPlay { position_ms }; } @@ -1090,9 +1166,9 @@ impl SpircTask { self.set_volume(volume); } - fn handle_end_of_track(&mut self) { + fn handle_end_of_track(&mut self) -> Result<(), Error> { self.handle_next(); - self.notify(None, true); + self.notify(None, true) } fn position(&mut self) -> u32 { @@ -1107,48 +1183,40 @@ impl SpircTask { } } - fn resolve_station(&self, uri: &str) -> BoxedFuture> { + fn resolve_station(&self, uri: &str) -> BoxedFuture> { let radio_uri = format!("hm://radio-apollo/v3/stations/{}", uri); self.resolve_uri(&radio_uri) } - fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture> { + fn resolve_autoplay_uri(&self, uri: &str) -> BoxedFuture> { let query_uri = format!("hm://autoplay-enabled/query?uri={}", uri); let request = self.session.mercury().get(query_uri); Box::pin( async { - let response = request.await?; + let response = request?.await?; if response.status_code == 200 { - let data = response - .payload - .first() - .expect("Empty autoplay uri") - .to_vec(); - let autoplay_uri = String::from_utf8(data).unwrap(); - Ok(autoplay_uri) + let data = response.payload.first().ok_or(SpircError::NoData)?.to_vec(); + Ok(String::from_utf8(data)?) } else { warn!("No autoplay_uri found"); - Err(MercuryError) + Err(MercuryError::Response(response).into()) } } .fuse(), ) } - fn resolve_uri(&self, uri: &str) -> BoxedFuture> { + fn resolve_uri(&self, uri: &str) -> BoxedFuture> { let request = self.session.mercury().get(uri); Box::pin( async move { - let response = request.await?; + let response = request?.await?; - let data = response - .payload - .first() - .expect("Empty payload on context uri"); - let response: serde_json::Value = serde_json::from_slice(data).unwrap(); + let data = response.payload.first().ok_or(SpircError::NoData)?; + let response: serde_json::Value = serde_json::from_slice(data)?; Ok(response) } @@ -1315,13 +1383,17 @@ impl SpircTask { } } - fn hello(&mut self) { - CommandSender::new(self, MessageType::kMessageTypeHello).send(); + fn hello(&mut self) -> Result<(), Error> { + CommandSender::new(self, MessageType::kMessageTypeHello).send() } - fn notify(&mut self, recipient: Option<&str>, suppress_loading_status: bool) { + fn notify( + &mut self, + recipient: Option<&str>, + suppress_loading_status: bool, + ) -> Result<(), Error> { if suppress_loading_status && (self.state.get_status() == PlayStatus::kPlayStatusLoading) { - return; + return Ok(()); }; let status_string = match self.state.get_status() { PlayStatus::kPlayStatusLoading => "kPlayStatusLoading", @@ -1334,7 +1406,7 @@ impl SpircTask { if let Some(s) = recipient { cs = cs.recipient(s); } - cs.send(); + cs.send() } fn set_volume(&mut self, volume: u16) { @@ -1382,11 +1454,11 @@ impl<'a> CommandSender<'a> { self } - fn send(mut self) { + fn send(mut self) -> Result<(), Error> { if !self.frame.has_state() && self.spirc.device.get_is_active() { self.frame.set_state(self.spirc.state.clone()); } - self.spirc.sender.send(self.frame.write_to_bytes().unwrap()); + self.spirc.sender.send(self.frame.write_to_bytes()?) } } diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index e78a272c..69a8e15c 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,7 +1,9 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + use hyper::{Body, Method, Request}; use serde::Deserialize; -use std::error::Error; -use std::sync::atomic::{AtomicUsize, Ordering}; + +use crate::Error; pub type SocketAddress = (String, u16); @@ -67,7 +69,7 @@ impl ApResolver { .collect() } - pub async fn try_apresolve(&self) -> Result> { + pub async fn try_apresolve(&self) -> Result { let req = Request::builder() .method(Method::GET) .uri("http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient") diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index 2198819e..74be4258 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -1,54 +1,85 @@ +use std::{collections::HashMap, io::Write}; + use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use bytes::Bytes; -use std::collections::HashMap; -use std::io::Write; +use thiserror::Error; use tokio::sync::oneshot; -use crate::file_id::FileId; -use crate::packet::PacketType; -use crate::spotify_id::SpotifyId; -use crate::util::SeqGenerator; +use crate::{packet::PacketType, util::SeqGenerator, Error, FileId, SpotifyId}; #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] pub struct AudioKey(pub [u8; 16]); -#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] -pub struct AudioKeyError; +#[derive(Debug, Error)] +pub enum AudioKeyError { + #[error("audio key error")] + AesKey, + #[error("other end of channel disconnected")] + Channel, + #[error("unexpected packet type {0}")] + Packet(u8), + #[error("sequence {0} not pending")] + Sequence(u32), +} + +impl From for Error { + fn from(err: AudioKeyError) -> Self { + match err { + AudioKeyError::AesKey => Error::unavailable(err), + AudioKeyError::Channel => Error::aborted(err), + AudioKeyError::Sequence(_) => Error::aborted(err), + AudioKeyError::Packet(_) => Error::unimplemented(err), + } + } +} component! { AudioKeyManager : AudioKeyManagerInner { sequence: SeqGenerator = SeqGenerator::new(0), - pending: HashMap>> = HashMap::new(), + pending: HashMap>> = HashMap::new(), } } impl AudioKeyManager { - pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> { let seq = BigEndian::read_u32(data.split_to(4).as_ref()); - let sender = self.lock(|inner| inner.pending.remove(&seq)); + let sender = self + .lock(|inner| inner.pending.remove(&seq)) + .ok_or(AudioKeyError::Sequence(seq))?; - if let Some(sender) = sender { - match cmd { - PacketType::AesKey => { - let mut key = [0u8; 16]; - key.copy_from_slice(data.as_ref()); - let _ = sender.send(Ok(AudioKey(key))); - } - PacketType::AesKeyError => { - warn!( - "error audio key {:x} {:x}", - data.as_ref()[0], - data.as_ref()[1] - ); - let _ = sender.send(Err(AudioKeyError)); - } - _ => (), + match cmd { + PacketType::AesKey => { + let mut key = [0u8; 16]; + key.copy_from_slice(data.as_ref()); + sender + .send(Ok(AudioKey(key))) + .map_err(|_| AudioKeyError::Channel)? + } + PacketType::AesKeyError => { + error!( + "error audio key {:x} {:x}", + data.as_ref()[0], + data.as_ref()[1] + ); + sender + .send(Err(AudioKeyError::AesKey.into())) + .map_err(|_| AudioKeyError::Channel)? + } + _ => { + trace!( + "Did not expect {:?} AES key packet with data {:#?}", + cmd, + data + ); + return Err(AudioKeyError::Packet(cmd as u8).into()); } } + + Ok(()) } - pub async fn request(&self, track: SpotifyId, file: FileId) -> Result { + pub async fn request(&self, track: SpotifyId, file: FileId) -> Result { let (tx, rx) = oneshot::channel(); let seq = self.lock(move |inner| { @@ -57,16 +88,16 @@ impl AudioKeyManager { seq }); - self.send_key_request(seq, track, file); - rx.await.map_err(|_| AudioKeyError)? + self.send_key_request(seq, track, file)?; + rx.await? } - fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) { + fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) -> Result<(), Error> { let mut data: Vec = Vec::new(); - data.write_all(&file.0).unwrap(); - data.write_all(&track.to_raw()).unwrap(); - data.write_u32::(seq).unwrap(); - data.write_u16::(0x0000).unwrap(); + data.write_all(&file.0)?; + data.write_all(&track.to_raw())?; + data.write_u32::(seq)?; + data.write_u16::(0x0000)?; self.session().send_packet(PacketType::RequestKey, data) } diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 3c188ecf..ad7cf331 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -7,8 +7,21 @@ use pbkdf2::pbkdf2; use protobuf::ProtobufEnum; use serde::{Deserialize, Serialize}; use sha1::{Digest, Sha1}; +use thiserror::Error; -use crate::protocol::authentication::AuthenticationType; +use crate::{protocol::authentication::AuthenticationType, Error}; + +#[derive(Debug, Error)] +pub enum AuthenticationError { + #[error("unknown authentication type {0}")] + AuthType(u32), +} + +impl From for Error { + fn from(err: AuthenticationError) -> Self { + Error::invalid_argument(err) + } +} /// The credentials are used to log into the Spotify API. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -46,7 +59,7 @@ impl Credentials { username: impl Into, encrypted_blob: impl AsRef<[u8]>, device_id: impl AsRef<[u8]>, - ) -> Credentials { + ) -> Result { fn read_u8(stream: &mut R) -> io::Result { let mut data = [0u8]; stream.read_exact(&mut data)?; @@ -91,7 +104,7 @@ impl Credentials { use aes::cipher::generic_array::GenericArray; use aes::cipher::{BlockCipher, NewBlockCipher}; - let mut data = base64::decode(encrypted_blob).unwrap(); + let mut data = base64::decode(encrypted_blob)?; let cipher = Aes192::new(GenericArray::from_slice(&key)); let block_size = ::BlockSize::to_usize(); @@ -109,19 +122,20 @@ impl Credentials { }; let mut cursor = io::Cursor::new(blob.as_slice()); - read_u8(&mut cursor).unwrap(); - read_bytes(&mut cursor).unwrap(); - read_u8(&mut cursor).unwrap(); - let auth_type = read_int(&mut cursor).unwrap(); - let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap(); - read_u8(&mut cursor).unwrap(); - let auth_data = read_bytes(&mut cursor).unwrap(); + read_u8(&mut cursor)?; + read_bytes(&mut cursor)?; + read_u8(&mut cursor)?; + let auth_type = read_int(&mut cursor)?; + let auth_type = AuthenticationType::from_i32(auth_type as i32) + .ok_or(AuthenticationError::AuthType(auth_type))?; + read_u8(&mut cursor)?; + let auth_data = read_bytes(&mut cursor)?; - Credentials { + Ok(Credentials { username, auth_type, auth_data, - } + }) } } diff --git a/core/src/cache.rs b/core/src/cache.rs index aec00e84..ed7cf83e 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -1,15 +1,29 @@ -use std::cmp::Reverse; -use std::collections::HashMap; -use std::fs::{self, File}; -use std::io::{self, Error, ErrorKind, Read, Write}; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; -use std::time::SystemTime; +use std::{ + cmp::Reverse, + collections::HashMap, + fs::{self, File}, + io::{self, Read, Write}, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + time::SystemTime, +}; use priority_queue::PriorityQueue; +use thiserror::Error; -use crate::authentication::Credentials; -use crate::file_id::FileId; +use crate::{authentication::Credentials, error::ErrorKind, Error, FileId}; + +#[derive(Debug, Error)] +pub enum CacheError { + #[error("audio cache location is not configured")] + Path, +} + +impl From for Error { + fn from(err: CacheError) -> Self { + Error::failed_precondition(err) + } +} /// Some kind of data structure that holds some paths, the size of these files and a timestamp. /// It keeps track of the file sizes and is able to pop the path with the oldest timestamp if @@ -57,16 +71,17 @@ impl SizeLimiter { /// to delete the file in the file system. fn pop(&mut self) -> Option { if self.exceeds_limit() { - let (next, _) = self - .queue - .pop() - .expect("in_use was > 0, so the queue should have contained an item."); - let size = self - .sizes - .remove(&next) - .expect("`queue` and `sizes` should have the same keys."); - self.in_use -= size; - Some(next) + if let Some((next, _)) = self.queue.pop() { + if let Some(size) = self.sizes.remove(&next) { + self.in_use -= size; + } else { + error!("`queue` and `sizes` should have the same keys."); + } + Some(next) + } else { + error!("in_use was > 0, so the queue should have contained an item."); + None + } } else { None } @@ -85,11 +100,11 @@ impl SizeLimiter { return false; } - let size = self - .sizes - .remove(file) - .expect("`queue` and `sizes` should have the same keys."); - self.in_use -= size; + if let Some(size) = self.sizes.remove(file) { + self.in_use -= size; + } else { + error!("`queue` and `sizes` should have the same keys."); + } true } @@ -172,56 +187,70 @@ impl FsSizeLimiter { } } - fn add(&self, file: &Path, size: u64) { + fn add(&self, file: &Path, size: u64) -> Result<(), Error> { self.limiter .lock() .unwrap() .add(file, size, SystemTime::now()); + Ok(()) } - fn touch(&self, file: &Path) -> bool { - self.limiter.lock().unwrap().update(file, SystemTime::now()) + fn touch(&self, file: &Path) -> Result { + Ok(self.limiter.lock().unwrap().update(file, SystemTime::now())) } - fn remove(&self, file: &Path) { - self.limiter.lock().unwrap().remove(file); + fn remove(&self, file: &Path) -> Result { + Ok(self.limiter.lock().unwrap().remove(file)) } - fn prune_internal Option>(mut pop: F) { + fn prune_internal Result, Error>>( + mut pop: F, + ) -> Result<(), Error> { let mut first = true; let mut count = 0; + let mut last_error = None; - while let Some(file) = pop() { - if first { - debug!("Cache dir exceeds limit, removing least recently used files."); - first = false; + while let Ok(result) = pop() { + if let Some(file) = result { + if first { + debug!("Cache dir exceeds limit, removing least recently used files."); + first = false; + } + + let res = fs::remove_file(&file); + if let Err(e) = res { + warn!("Could not remove file {:?} from cache dir: {}", file, e); + last_error = Some(e); + } else { + count += 1; + } } - if let Err(e) = fs::remove_file(&file) { - warn!("Could not remove file {:?} from cache dir: {}", file, e); - } else { - count += 1; + if count > 0 { + info!("Removed {} cache files.", count); } } - if count > 0 { - info!("Removed {} cache files.", count); + if let Some(err) = last_error { + Err(err.into()) + } else { + Ok(()) } } - fn prune(&self) { - Self::prune_internal(|| self.limiter.lock().unwrap().pop()) + fn prune(&self) -> Result<(), Error> { + Self::prune_internal(|| Ok(self.limiter.lock().unwrap().pop())) } - fn new(path: &Path, limit: u64) -> Self { + fn new(path: &Path, limit: u64) -> Result { let mut limiter = SizeLimiter::new(limit); Self::init_dir(&mut limiter, path); - Self::prune_internal(|| limiter.pop()); + Self::prune_internal(|| Ok(limiter.pop()))?; - Self { + Ok(Self { limiter: Mutex::new(limiter), - } + }) } } @@ -234,15 +263,13 @@ pub struct Cache { size_limiter: Option>, } -pub struct RemoveFileError(()); - impl Cache { pub fn new>( credentials_path: Option

, volume_path: Option

, audio_path: Option

, size_limit: Option, - ) -> io::Result { + ) -> Result { let mut size_limiter = None; if let Some(location) = &credentials_path { @@ -263,8 +290,7 @@ impl Cache { fs::create_dir_all(location)?; if let Some(limit) = size_limit { - let limiter = FsSizeLimiter::new(location.as_ref(), limit); - + let limiter = FsSizeLimiter::new(location.as_ref(), limit)?; size_limiter = Some(Arc::new(limiter)); } } @@ -285,11 +311,11 @@ impl Cache { let location = self.credentials_location.as_ref()?; // This closure is just convencience to enable the question mark operator - let read = || { + let read = || -> Result { let mut file = File::open(location)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; - serde_json::from_str(&contents).map_err(|e| Error::new(ErrorKind::InvalidData, e)) + Ok(serde_json::from_str(&contents)?) }; match read() { @@ -297,7 +323,7 @@ impl Cache { Err(e) => { // If the file did not exist, the file was probably not written // before. Otherwise, log the error. - if e.kind() != ErrorKind::NotFound { + if e.kind != ErrorKind::NotFound { warn!("Error reading credentials from cache: {}", e); } None @@ -321,19 +347,17 @@ impl Cache { pub fn volume(&self) -> Option { let location = self.volume_location.as_ref()?; - let read = || { + let read = || -> Result { let mut file = File::open(location)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; - contents - .parse() - .map_err(|e| Error::new(ErrorKind::InvalidData, e)) + Ok(contents.parse()?) }; match read() { Ok(v) => Some(v), Err(e) => { - if e.kind() != ErrorKind::NotFound { + if e.kind != ErrorKind::NotFound { warn!("Error reading volume from cache: {}", e); } None @@ -364,12 +388,14 @@ impl Cache { match File::open(&path) { Ok(file) => { if let Some(limiter) = self.size_limiter.as_deref() { - limiter.touch(&path); + if let Err(e) = limiter.touch(&path) { + error!("limiter could not touch {:?}: {}", path, e); + } } Some(file) } Err(e) => { - if e.kind() != ErrorKind::NotFound { + if e.kind() != io::ErrorKind::NotFound { warn!("Error reading file from cache: {}", e) } None @@ -377,7 +403,7 @@ impl Cache { } } - pub fn save_file(&self, file: FileId, contents: &mut F) -> bool { + pub fn save_file(&self, file: FileId, contents: &mut F) -> Result<(), Error> { if let Some(path) = self.file_path(file) { if let Some(parent) = path.parent() { if let Ok(size) = fs::create_dir_all(parent) @@ -385,28 +411,25 @@ impl Cache { .and_then(|mut file| io::copy(contents, &mut file)) { if let Some(limiter) = self.size_limiter.as_deref() { - limiter.add(&path, size); - limiter.prune(); + limiter.add(&path, size)?; + limiter.prune()? } - return true; + return Ok(()); } } } - false + Err(CacheError::Path.into()) } - pub fn remove_file(&self, file: FileId) -> Result<(), RemoveFileError> { - let path = self.file_path(file).ok_or(RemoveFileError(()))?; + pub fn remove_file(&self, file: FileId) -> Result<(), Error> { + let path = self.file_path(file).ok_or(CacheError::Path)?; - if let Err(err) = fs::remove_file(&path) { - warn!("Unable to remove file from cache: {}", err); - Err(RemoveFileError(())) - } else { - if let Some(limiter) = self.size_limiter.as_deref() { - limiter.remove(&path); - } - Ok(()) + fs::remove_file(&path)?; + if let Some(limiter) = self.size_limiter.as_deref() { + limiter.remove(&path)?; } + + Ok(()) } } diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 13f23a37..409d7f25 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -1,34 +1,19 @@ +use std::{ + convert::{TryFrom, TryInto}, + ops::{Deref, DerefMut}, +}; + use chrono::Local; -use protobuf::{Message, ProtobufError}; +use protobuf::Message; use thiserror::Error; use url::Url; -use std::convert::{TryFrom, TryInto}; -use std::ops::{Deref, DerefMut}; - -use super::date::Date; -use super::file_id::FileId; -use super::session::Session; -use super::spclient::SpClientError; +use super::{date::Date, Error, FileId, Session}; use librespot_protocol as protocol; use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage; use protocol::storage_resolve::StorageResolveResponse_Result; -#[derive(Error, Debug)] -pub enum CdnUrlError { - #[error("no URLs available")] - Empty, - #[error("all tokens expired")] - Expired, - #[error("error parsing response")] - Parsing, - #[error("could not parse protobuf: {0}")] - Protobuf(#[from] ProtobufError), - #[error("could not complete API request: {0}")] - SpClient(#[from] SpClientError), -} - #[derive(Debug, Clone)] pub struct MaybeExpiringUrl(pub String, pub Option); @@ -48,10 +33,27 @@ impl DerefMut for MaybeExpiringUrls { } } +#[derive(Debug, Error)] +pub enum CdnUrlError { + #[error("all URLs expired")] + Expired, + #[error("resolved storage is not for CDN")] + Storage, +} + +impl From for Error { + fn from(err: CdnUrlError) -> Self { + match err { + CdnUrlError::Expired => Error::deadline_exceeded(err), + CdnUrlError::Storage => Error::unavailable(err), + } + } +} + #[derive(Debug, Clone)] pub struct CdnUrl { pub file_id: FileId, - pub urls: MaybeExpiringUrls, + urls: MaybeExpiringUrls, } impl CdnUrl { @@ -62,7 +64,7 @@ impl CdnUrl { } } - pub async fn resolve_audio(&self, session: &Session) -> Result { + pub async fn resolve_audio(&self, session: &Session) -> Result { let file_id = self.file_id; let response = session.spclient().get_audio_urls(file_id).await?; let msg = CdnUrlMessage::parse_from_bytes(&response)?; @@ -75,37 +77,26 @@ impl CdnUrl { Ok(cdn_url) } - pub fn get_url(&mut self) -> Result<&str, CdnUrlError> { - if self.urls.is_empty() { - return Err(CdnUrlError::Empty); - } - - // prune expired URLs until the first one is current, or none are left + pub fn try_get_url(&self) -> Result<&str, Error> { let now = Local::now(); - while !self.urls.is_empty() { - let maybe_expiring = self.urls[0].1; - if let Some(expiry) = maybe_expiring { - if now < expiry.as_utc() { - break; - } else { - self.urls.remove(0); - } - } - } + let url = self.urls.iter().find(|url| match url.1 { + Some(expiry) => now < expiry.as_utc(), + None => true, + }); - if let Some(cdn_url) = self.urls.first() { - Ok(&cdn_url.0) + if let Some(url) = url { + Ok(&url.0) } else { - Err(CdnUrlError::Expired) + Err(CdnUrlError::Expired.into()) } } } impl TryFrom for MaybeExpiringUrls { - type Error = CdnUrlError; + type Error = crate::Error; fn try_from(msg: CdnUrlMessage) -> Result { if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) { - return Err(CdnUrlError::Parsing); + return Err(CdnUrlError::Storage.into()); } let is_expiring = !msg.get_fileid().is_empty(); @@ -114,7 +105,7 @@ impl TryFrom for MaybeExpiringUrls { .get_cdnurl() .iter() .map(|cdn_url| { - let url = Url::parse(cdn_url).map_err(|_| CdnUrlError::Parsing)?; + let url = Url::parse(cdn_url)?; if is_expiring { let expiry_str = if let Some(token) = url @@ -122,29 +113,47 @@ impl TryFrom for MaybeExpiringUrls { .into_iter() .find(|(key, _value)| key == "__token__") { - let start = token.1.find("exp=").ok_or(CdnUrlError::Parsing)?; - let slice = &token.1[start + 4..]; - let end = slice.find('~').ok_or(CdnUrlError::Parsing)?; - String::from(&slice[..end]) + if let Some(mut start) = token.1.find("exp=") { + start += 4; + if token.1.len() >= start { + let slice = &token.1[start..]; + if let Some(end) = slice.find('~') { + // this is the only valid invariant for akamaized.net + String::from(&slice[..end]) + } else { + String::from(slice) + } + } else { + String::new() + } + } else { + String::new() + } } else if let Some(query) = url.query() { let mut items = query.split('_'); - String::from(items.next().ok_or(CdnUrlError::Parsing)?) + if let Some(first) = items.next() { + // this is the only valid invariant for scdn.co + String::from(first) + } else { + String::new() + } } else { - return Err(CdnUrlError::Parsing); + String::new() }; - let mut expiry: i64 = expiry_str.parse().map_err(|_| CdnUrlError::Parsing)?; + let mut expiry: i64 = expiry_str.parse()?; + expiry -= 5 * 60; // seconds Ok(MaybeExpiringUrl( cdn_url.to_owned(), - Some(expiry.try_into().map_err(|_| CdnUrlError::Parsing)?), + Some(expiry.try_into()?), )) } else { Ok(MaybeExpiringUrl(cdn_url.to_owned(), None)) } }) - .collect::, CdnUrlError>>()?; + .collect::, Error>>()?; Ok(Self(result)) } diff --git a/core/src/channel.rs b/core/src/channel.rs index 31c01a40..607189a0 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -1,18 +1,20 @@ -use std::collections::HashMap; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::time::Instant; +use std::{ + collections::HashMap, + fmt, + pin::Pin, + task::{Context, Poll}, + time::Instant, +}; use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use futures_core::Stream; -use futures_util::lock::BiLock; -use futures_util::{ready, StreamExt}; +use futures_util::{lock::BiLock, ready, StreamExt}; use num_traits::FromPrimitive; +use thiserror::Error; use tokio::sync::mpsc; -use crate::packet::PacketType; -use crate::util::SeqGenerator; +use crate::{packet::PacketType, util::SeqGenerator, Error}; component! { ChannelManager : ChannelManagerInner { @@ -27,9 +29,21 @@ component! { const ONE_SECOND_IN_MS: usize = 1000; -#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] +#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)] pub struct ChannelError; +impl From for Error { + fn from(err: ChannelError) -> Self { + Error::aborted(err) + } +} + +impl fmt::Display for ChannelError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "channel error") + } +} + pub struct Channel { receiver: mpsc::UnboundedReceiver<(u8, Bytes)>, state: ChannelState, @@ -70,7 +84,7 @@ impl ChannelManager { (seq, channel) } - pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> { use std::collections::hash_map::Entry; let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref()); @@ -94,9 +108,14 @@ impl ChannelManager { inner.download_measurement_bytes += data.len(); if let Entry::Occupied(entry) = inner.channels.entry(id) { - let _ = entry.get().send((cmd as u8, data)); + entry + .get() + .send((cmd as u8, data)) + .map_err(|_| ChannelError)?; } - }); + + Ok(()) + }) } pub fn get_download_rate_estimate(&self) -> usize { @@ -142,7 +161,11 @@ impl Stream for Channel { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { loop { match self.state.clone() { - ChannelState::Closed => panic!("Polling already terminated channel"), + ChannelState::Closed => { + error!("Polling already terminated channel"); + return Poll::Ready(None); + } + ChannelState::Header(mut data) => { if data.is_empty() { data = ready!(self.recv_packet(cx))?; diff --git a/core/src/component.rs b/core/src/component.rs index a761c455..aa1da840 100644 --- a/core/src/component.rs +++ b/core/src/component.rs @@ -14,7 +14,7 @@ macro_rules! component { #[allow(dead_code)] fn lock R, R>(&self, f: F) -> R { - let mut inner = (self.0).1.lock().expect("Mutex poisoned"); + let mut inner = (self.0).1.lock().unwrap(); f(&mut inner) } diff --git a/core/src/config.rs b/core/src/config.rs index c6b3d23c..f04326ae 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -1,6 +1,5 @@ -use std::fmt; -use std::path::PathBuf; -use std::str::FromStr; +use std::{fmt, path::PathBuf, str::FromStr}; + use url::Url; #[derive(Clone, Debug)] diff --git a/core/src/connection/codec.rs b/core/src/connection/codec.rs index 86533aaf..826839c6 100644 --- a/core/src/connection/codec.rs +++ b/core/src/connection/codec.rs @@ -1,12 +1,20 @@ +use std::io; + use byteorder::{BigEndian, ByteOrder}; use bytes::{BufMut, Bytes, BytesMut}; use shannon::Shannon; -use std::io; +use thiserror::Error; use tokio_util::codec::{Decoder, Encoder}; const HEADER_SIZE: usize = 3; const MAC_SIZE: usize = 4; +#[derive(Debug, Error)] +pub enum ApCodecError { + #[error("payload was malformed")] + Payload, +} + #[derive(Debug)] enum DecodeState { Header, @@ -87,7 +95,10 @@ impl Decoder for ApCodec { let mut payload = buf.split_to(size + MAC_SIZE); - self.decode_cipher.decrypt(payload.get_mut(..size).unwrap()); + self.decode_cipher + .decrypt(payload.get_mut(..size).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, ApCodecError::Payload) + })?); let mac = payload.split_off(size); self.decode_cipher.check_mac(mac.as_ref())?; diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 8acc0d01..42d64df2 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -1,20 +1,28 @@ +use std::{env::consts::ARCH, io}; + use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use hmac::{Hmac, Mac, NewMac}; use protobuf::{self, Message}; use rand::{thread_rng, RngCore}; use sha1::Sha1; -use std::env::consts::ARCH; -use std::io; +use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio_util::codec::{Decoder, Framed}; use super::codec::ApCodec; -use crate::diffie_hellman::DhLocalKeys; + +use crate::{diffie_hellman::DhLocalKeys, version}; + use crate::protocol; use crate::protocol::keyexchange::{ APResponseMessage, ClientHello, ClientResponsePlaintext, Platform, ProductFlags, }; -use crate::version; + +#[derive(Debug, Error)] +pub enum HandshakeError { + #[error("invalid key length")] + InvalidLength, +} pub async fn handshake( mut connection: T, @@ -31,7 +39,7 @@ pub async fn handshake( .to_owned(); let shared_secret = local_keys.shared_secret(&remote_key); - let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator); + let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator)?; let codec = ApCodec::new(&send_key, &recv_key); client_response(&mut connection, challenge).await?; @@ -112,8 +120,8 @@ where let mut buffer = vec![0, 4]; let size = 2 + 4 + packet.compute_size(); - as WriteBytesExt>::write_u32::(&mut buffer, size).unwrap(); - packet.write_to_vec(&mut buffer).unwrap(); + as WriteBytesExt>::write_u32::(&mut buffer, size)?; + packet.write_to_vec(&mut buffer)?; connection.write_all(&buffer[..]).await?; Ok(buffer) @@ -133,8 +141,8 @@ where let mut buffer = vec![]; let size = 4 + packet.compute_size(); - as WriteBytesExt>::write_u32::(&mut buffer, size).unwrap(); - packet.write_to_vec(&mut buffer).unwrap(); + as WriteBytesExt>::write_u32::(&mut buffer, size)?; + packet.write_to_vec(&mut buffer)?; connection.write_all(&buffer[..]).await?; Ok(()) @@ -148,7 +156,7 @@ where let header = read_into_accumulator(connection, 4, acc).await?; let size = BigEndian::read_u32(header) as usize; let data = read_into_accumulator(connection, size - 4, acc).await?; - let message = M::parse_from_bytes(data).unwrap(); + let message = M::parse_from_bytes(data)?; Ok(message) } @@ -164,24 +172,26 @@ async fn read_into_accumulator<'a, 'b, T: AsyncRead + Unpin>( Ok(&mut acc[offset..]) } -fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec, Vec, Vec) { +fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> io::Result<(Vec, Vec, Vec)> { type HmacSha1 = Hmac; let mut data = Vec::with_capacity(0x64); for i in 1..6 { - let mut mac = - HmacSha1::new_from_slice(shared_secret).expect("HMAC can take key of any size"); + let mut mac = HmacSha1::new_from_slice(shared_secret).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength) + })?; mac.update(packets); mac.update(&[i]); data.extend_from_slice(&mac.finalize().into_bytes()); } - let mut mac = HmacSha1::new_from_slice(&data[..0x14]).expect("HMAC can take key of any size"); + let mut mac = HmacSha1::new_from_slice(&data[..0x14]) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, HandshakeError::InvalidLength))?; mac.update(packets); - ( + Ok(( mac.finalize().into_bytes().to_vec(), data[0x14..0x34].to_vec(), data[0x34..0x54].to_vec(), - ) + )) } diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 29a33296..0b59de88 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -1,23 +1,21 @@ mod codec; mod handshake; -pub use self::codec::ApCodec; -pub use self::handshake::handshake; +pub use self::{codec::ApCodec, handshake::handshake}; -use std::io::{self, ErrorKind}; +use std::io; use futures_util::{SinkExt, StreamExt}; use num_traits::FromPrimitive; -use protobuf::{self, Message, ProtobufError}; +use protobuf::{self, Message}; use thiserror::Error; use tokio::net::TcpStream; use tokio_util::codec::Framed; use url::Url; -use crate::authentication::Credentials; -use crate::packet::PacketType; +use crate::{authentication::Credentials, packet::PacketType, version, Error}; + use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; -use crate::version; pub type Transport = Framed; @@ -42,13 +40,19 @@ fn login_error_message(code: &ErrorCode) -> &'static str { pub enum AuthenticationError { #[error("Login failed with reason: {}", login_error_message(.0))] LoginFailed(ErrorCode), - #[error("Authentication failed: {0}")] - IoError(#[from] io::Error), + #[error("invalid packet {0}")] + Packet(u8), + #[error("transport returned no data")] + Transport, } -impl From for AuthenticationError { - fn from(e: ProtobufError) -> Self { - io::Error::new(ErrorKind::InvalidData, e).into() +impl From for Error { + fn from(err: AuthenticationError) -> Self { + match err { + AuthenticationError::LoginFailed(_) => Error::permission_denied(err), + AuthenticationError::Packet(_) => Error::unimplemented(err), + AuthenticationError::Transport => Error::unavailable(err), + } } } @@ -68,7 +72,7 @@ pub async fn authenticate( transport: &mut Transport, credentials: Credentials, device_id: &str, -) -> Result { +) -> Result { use crate::protocol::authentication::{APWelcome, ClientResponseEncrypted, CpuFamily, Os}; let cpu_family = match std::env::consts::ARCH { @@ -119,12 +123,15 @@ pub async fn authenticate( packet.set_version_string(format!("librespot {}", version::SEMVER)); let cmd = PacketType::Login; - let data = packet.write_to_bytes().unwrap(); + let data = packet.write_to_bytes()?; transport.send((cmd as u8, data)).await?; - let (cmd, data) = transport.next().await.expect("EOF")?; + let (cmd, data) = transport + .next() + .await + .ok_or(AuthenticationError::Transport)??; let packet_type = FromPrimitive::from_u8(cmd); - match packet_type { + let result = match packet_type { Some(PacketType::APWelcome) => { let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?; @@ -141,8 +148,13 @@ pub async fn authenticate( Err(error_data.into()) } _ => { - let msg = format!("Received invalid packet: {}", cmd); - Err(io::Error::new(ErrorKind::InvalidData, msg).into()) + trace!( + "Did not expect {:?} AES key packet with data {:#?}", + cmd, + data + ); + Err(AuthenticationError::Packet(cmd)) } - } + }; + Ok(result?) } diff --git a/core/src/date.rs b/core/src/date.rs index a84da606..fe052299 100644 --- a/core/src/date.rs +++ b/core/src/date.rs @@ -1,18 +1,23 @@ -use std::convert::TryFrom; -use std::fmt::Debug; -use std::ops::Deref; +use std::{convert::TryFrom, fmt::Debug, ops::Deref}; -use chrono::{DateTime, Utc}; -use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; +use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; use thiserror::Error; +use crate::Error; + use librespot_protocol as protocol; use protocol::metadata::Date as DateMessage; #[derive(Debug, Error)] pub enum DateError { - #[error("item has invalid date")] - InvalidTimestamp, + #[error("item has invalid timestamp {0}")] + Timestamp(i64), +} + +impl From for Error { + fn from(err: DateError) -> Self { + Error::invalid_argument(err) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -30,11 +35,11 @@ impl Date { self.0.timestamp() } - pub fn from_timestamp(timestamp: i64) -> Result { + pub fn from_timestamp(timestamp: i64) -> Result { if let Some(date_time) = NaiveDateTime::from_timestamp_opt(timestamp, 0) { Ok(Self::from_utc(date_time)) } else { - Err(DateError::InvalidTimestamp) + Err(DateError::Timestamp(timestamp).into()) } } @@ -67,7 +72,7 @@ impl From> for Date { } impl TryFrom for Date { - type Error = DateError; + type Error = crate::Error; fn try_from(timestamp: i64) -> Result { Self::from_timestamp(timestamp) } diff --git a/core/src/dealer/maps.rs b/core/src/dealer/maps.rs index 38916e40..4f719de7 100644 --- a/core/src/dealer/maps.rs +++ b/core/src/dealer/maps.rs @@ -1,7 +1,20 @@ use std::collections::HashMap; -#[derive(Debug)] -pub struct AlreadyHandledError(()); +use thiserror::Error; + +use crate::Error; + +#[derive(Debug, Error)] +pub enum HandlerMapError { + #[error("request was already handled")] + AlreadyHandled, +} + +impl From for Error { + fn from(err: HandlerMapError) -> Self { + Error::aborted(err) + } +} pub enum HandlerMap { Leaf(T), @@ -19,9 +32,9 @@ impl HandlerMap { &mut self, mut path: impl Iterator, handler: T, - ) -> Result<(), AlreadyHandledError> { + ) -> Result<(), Error> { match self { - Self::Leaf(_) => Err(AlreadyHandledError(())), + Self::Leaf(_) => Err(HandlerMapError::AlreadyHandled.into()), Self::Branch(children) => { if let Some(component) = path.next() { let node = children.entry(component.to_owned()).or_default(); @@ -30,7 +43,7 @@ impl HandlerMap { *self = Self::Leaf(handler); Ok(()) } else { - Err(AlreadyHandledError(())) + Err(HandlerMapError::AlreadyHandled.into()) } } } diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index ba1e68df..ac19fd6d 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -1,29 +1,40 @@ mod maps; pub mod protocol; -use std::iter; -use std::pin::Pin; -use std::sync::atomic::AtomicBool; -use std::sync::{atomic, Arc, Mutex}; -use std::task::Poll; -use std::time::Duration; +use std::{ + iter, + pin::Pin, + sync::{ + atomic::{self, AtomicBool}, + Arc, Mutex, + }, + task::Poll, + time::Duration, +}; use futures_core::{Future, Stream}; -use futures_util::future::join_all; -use futures_util::{SinkExt, StreamExt}; +use futures_util::{future::join_all, SinkExt, StreamExt}; use thiserror::Error; -use tokio::select; -use tokio::sync::mpsc::{self, UnboundedReceiver}; -use tokio::sync::Semaphore; -use tokio::task::JoinHandle; +use tokio::{ + select, + sync::{ + mpsc::{self, UnboundedReceiver}, + Semaphore, + }, + task::JoinHandle, +}; use tokio_tungstenite::tungstenite; use tungstenite::error::UrlError; use url::Url; use self::maps::*; use self::protocol::*; -use crate::socket; -use crate::util::{keep_flushing, CancelOnDrop, TimeoutOnDrop}; + +use crate::{ + socket, + util::{keep_flushing, CancelOnDrop, TimeoutOnDrop}, + Error, +}; type WsMessage = tungstenite::Message; type WsError = tungstenite::Error; @@ -164,24 +175,38 @@ fn split_uri(s: &str) -> Option> { pub enum AddHandlerError { #[error("There is already a handler for the given uri")] AlreadyHandled, - #[error("The specified uri is invalid")] - InvalidUri, + #[error("The specified uri {0} is invalid")] + InvalidUri(String), +} + +impl From for Error { + fn from(err: AddHandlerError) -> Self { + match err { + AddHandlerError::AlreadyHandled => Error::aborted(err), + AddHandlerError::InvalidUri(_) => Error::invalid_argument(err), + } + } } #[derive(Debug, Clone, Error)] pub enum SubscriptionError { #[error("The specified uri is invalid")] - InvalidUri, + InvalidUri(String), +} + +impl From for Error { + fn from(err: SubscriptionError) -> Self { + Error::invalid_argument(err) + } } fn add_handler( map: &mut HandlerMap>, uri: &str, handler: impl RequestHandler, -) -> Result<(), AddHandlerError> { - let split = split_uri(uri).ok_or(AddHandlerError::InvalidUri)?; +) -> Result<(), Error> { + let split = split_uri(uri).ok_or_else(|| AddHandlerError::InvalidUri(uri.to_string()))?; map.insert(split, Box::new(handler)) - .map_err(|_| AddHandlerError::AlreadyHandled) } fn remove_handler(map: &mut HandlerMap, uri: &str) -> Option { @@ -191,11 +216,11 @@ fn remove_handler(map: &mut HandlerMap, uri: &str) -> Option { fn subscribe( map: &mut SubscriberMap, uris: &[&str], -) -> Result { +) -> Result { let (tx, rx) = mpsc::unbounded_channel(); for &uri in uris { - let split = split_uri(uri).ok_or(SubscriptionError::InvalidUri)?; + let split = split_uri(uri).ok_or_else(|| SubscriptionError::InvalidUri(uri.to_string()))?; map.insert(split, tx.clone()); } @@ -237,15 +262,11 @@ impl Builder { Self::default() } - pub fn add_handler( - &mut self, - uri: &str, - handler: impl RequestHandler, - ) -> Result<(), AddHandlerError> { + pub fn add_handler(&mut self, uri: &str, handler: impl RequestHandler) -> Result<(), Error> { add_handler(&mut self.request_handlers, uri, handler) } - pub fn subscribe(&mut self, uris: &[&str]) -> Result { + pub fn subscribe(&mut self, uris: &[&str]) -> Result { subscribe(&mut self.message_handlers, uris) } @@ -342,7 +363,7 @@ pub struct Dealer { } impl Dealer { - pub fn add_handler(&self, uri: &str, handler: H) -> Result<(), AddHandlerError> + pub fn add_handler(&self, uri: &str, handler: H) -> Result<(), Error> where H: RequestHandler, { @@ -357,7 +378,7 @@ impl Dealer { remove_handler(&mut self.shared.request_handlers.lock().unwrap(), uri) } - pub fn subscribe(&self, uris: &[&str]) -> Result { + pub fn subscribe(&self, uris: &[&str]) -> Result { subscribe(&mut self.shared.message_handlers.lock().unwrap(), uris) } @@ -367,7 +388,9 @@ impl Dealer { self.shared.notify_drop.close(); if let Some(handle) = self.handle.take() { - CancelOnDrop(handle).await.unwrap(); + if let Err(e) = CancelOnDrop(handle).await { + error!("error aborting dealer operations: {}", e); + } } } } @@ -556,11 +579,15 @@ async fn run( select! { () = shared.closed() => break, r = t0 => { - r.unwrap(); // Whatever has gone wrong (probably panicked), we can't handle it, so let's panic too. + if let Err(e) = r { + error!("timeout on task 0: {}", e); + } tasks.0.take(); }, r = t1 => { - r.unwrap(); + if let Err(e) = r { + error!("timeout on task 1: {}", e); + } tasks.1.take(); } } @@ -576,7 +603,7 @@ async fn run( match connect(&url, proxy.as_ref(), &shared).await { Ok((s, r)) => tasks = (init_task(s), init_task(r)), Err(e) => { - warn!("Error while connecting: {}", e); + error!("Error while connecting: {}", e); tokio::time::sleep(RECONNECT_INTERVAL).await; } } diff --git a/core/src/error.rs b/core/src/error.rs new file mode 100644 index 00000000..e3753014 --- /dev/null +++ b/core/src/error.rs @@ -0,0 +1,437 @@ +use std::{error, fmt, num::ParseIntError, str::Utf8Error, string::FromUtf8Error}; + +use base64::DecodeError; +use http::{ + header::{InvalidHeaderName, InvalidHeaderValue, ToStrError}, + method::InvalidMethod, + status::InvalidStatusCode, + uri::{InvalidUri, InvalidUriParts}, +}; +use protobuf::ProtobufError; +use thiserror::Error; +use tokio::sync::{mpsc::error::SendError, oneshot::error::RecvError}; +use url::ParseError; + +#[derive(Debug)] +pub struct Error { + pub kind: ErrorKind, + pub error: Box, +} + +#[derive(Clone, Copy, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)] +pub enum ErrorKind { + #[error("The operation was cancelled by the caller")] + Cancelled = 1, + + #[error("Unknown error")] + Unknown = 2, + + #[error("Client specified an invalid argument")] + InvalidArgument = 3, + + #[error("Deadline expired before operation could complete")] + DeadlineExceeded = 4, + + #[error("Requested entity was not found")] + NotFound = 5, + + #[error("Attempt to create entity that already exists")] + AlreadyExists = 6, + + #[error("Permission denied")] + PermissionDenied = 7, + + #[error("No valid authentication credentials")] + Unauthenticated = 16, + + #[error("Resource has been exhausted")] + ResourceExhausted = 8, + + #[error("Invalid state")] + FailedPrecondition = 9, + + #[error("Operation aborted")] + Aborted = 10, + + #[error("Operation attempted past the valid range")] + OutOfRange = 11, + + #[error("Not implemented")] + Unimplemented = 12, + + #[error("Internal error")] + Internal = 13, + + #[error("Service unavailable")] + Unavailable = 14, + + #[error("Unrecoverable data loss or corruption")] + DataLoss = 15, + + #[error("Operation must not be used")] + DoNotUse = -1, +} + +#[derive(Debug, Error)] +struct ErrorMessage(String); + +impl fmt::Display for ErrorMessage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Error { + pub fn new(kind: ErrorKind, error: E) -> Error + where + E: Into>, + { + Self { + kind, + error: error.into(), + } + } + + pub fn aborted(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Aborted, + error: error.into(), + } + } + + pub fn already_exists(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::AlreadyExists, + error: error.into(), + } + } + + pub fn cancelled(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Cancelled, + error: error.into(), + } + } + + pub fn data_loss(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::DataLoss, + error: error.into(), + } + } + + pub fn deadline_exceeded(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::DeadlineExceeded, + error: error.into(), + } + } + + pub fn do_not_use(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::DoNotUse, + error: error.into(), + } + } + + pub fn failed_precondition(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::FailedPrecondition, + error: error.into(), + } + } + + pub fn internal(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Internal, + error: error.into(), + } + } + + pub fn invalid_argument(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::InvalidArgument, + error: error.into(), + } + } + + pub fn not_found(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::NotFound, + error: error.into(), + } + } + + pub fn out_of_range(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::OutOfRange, + error: error.into(), + } + } + + pub fn permission_denied(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::PermissionDenied, + error: error.into(), + } + } + + pub fn resource_exhausted(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::ResourceExhausted, + error: error.into(), + } + } + + pub fn unauthenticated(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Unauthenticated, + error: error.into(), + } + } + + pub fn unavailable(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Unavailable, + error: error.into(), + } + } + + pub fn unimplemented(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Unimplemented, + error: error.into(), + } + } + + pub fn unknown(error: E) -> Error + where + E: Into>, + { + Self { + kind: ErrorKind::Unknown, + error: error.into(), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.error.source() + } +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "{} {{ ", self.kind)?; + self.error.fmt(fmt)?; + write!(fmt, " }}") + } +} + +impl From for Error { + fn from(err: DecodeError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: http::Error) -> Self { + if err.is::() + || err.is::() + || err.is::() + || err.is::() + || err.is::() + { + return Self::new(ErrorKind::InvalidArgument, err); + } + + if err.is::() { + return Self::new(ErrorKind::FailedPrecondition, err); + } + + Self::new(ErrorKind::Unknown, err) + } +} + +impl From for Error { + fn from(err: hyper::Error) -> Self { + if err.is_parse() || err.is_parse_too_large() || err.is_parse_status() || err.is_user() { + return Self::new(ErrorKind::Internal, err); + } + + if err.is_canceled() { + return Self::new(ErrorKind::Cancelled, err); + } + + if err.is_connect() { + return Self::new(ErrorKind::Unavailable, err); + } + + if err.is_incomplete_message() { + return Self::new(ErrorKind::DataLoss, err); + } + + if err.is_body_write_aborted() || err.is_closed() { + return Self::new(ErrorKind::Aborted, err); + } + + if err.is_timeout() { + return Self::new(ErrorKind::DeadlineExceeded, err); + } + + Self::new(ErrorKind::Unknown, err) + } +} + +impl From for Error { + fn from(err: quick_xml::Error) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: serde_json::Error) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + use std::io::ErrorKind as IoErrorKind; + match err.kind() { + IoErrorKind::NotFound => Self::new(ErrorKind::NotFound, err), + IoErrorKind::PermissionDenied => Self::new(ErrorKind::PermissionDenied, err), + IoErrorKind::AddrInUse | IoErrorKind::AlreadyExists => { + Self::new(ErrorKind::AlreadyExists, err) + } + IoErrorKind::AddrNotAvailable + | IoErrorKind::ConnectionRefused + | IoErrorKind::NotConnected => Self::new(ErrorKind::Unavailable, err), + IoErrorKind::BrokenPipe + | IoErrorKind::ConnectionReset + | IoErrorKind::ConnectionAborted => Self::new(ErrorKind::Aborted, err), + IoErrorKind::Interrupted | IoErrorKind::WouldBlock => { + Self::new(ErrorKind::Cancelled, err) + } + IoErrorKind::InvalidData | IoErrorKind::UnexpectedEof => { + Self::new(ErrorKind::FailedPrecondition, err) + } + IoErrorKind::TimedOut => Self::new(ErrorKind::DeadlineExceeded, err), + IoErrorKind::InvalidInput => Self::new(ErrorKind::InvalidArgument, err), + IoErrorKind::WriteZero => Self::new(ErrorKind::ResourceExhausted, err), + _ => Self::new(ErrorKind::Unknown, err), + } + } +} + +impl From for Error { + fn from(err: FromUtf8Error) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: InvalidHeaderValue) -> Self { + Self::new(ErrorKind::InvalidArgument, err) + } +} + +impl From for Error { + fn from(err: InvalidUri) -> Self { + Self::new(ErrorKind::InvalidArgument, err) + } +} + +impl From for Error { + fn from(err: ParseError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: ParseIntError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: ProtobufError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: RecvError) -> Self { + Self::new(ErrorKind::Internal, err) + } +} + +impl From> for Error { + fn from(err: SendError) -> Self { + Self { + kind: ErrorKind::Internal, + error: ErrorMessage(err.to_string()).into(), + } + } +} + +impl From for Error { + fn from(err: ToStrError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + +impl From for Error { + fn from(err: Utf8Error) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} diff --git a/core/src/file_id.rs b/core/src/file_id.rs index f6e385cd..79969848 100644 --- a/core/src/file_id.rs +++ b/core/src/file_id.rs @@ -1,7 +1,7 @@ -use librespot_protocol as protocol; - use std::fmt; +use librespot_protocol as protocol; + use crate::spotify_id::to_base16; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 52206c5c..2dc21355 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,49 +1,82 @@ +use std::env::consts::OS; + use bytes::Bytes; -use futures_util::future::IntoStream; -use futures_util::FutureExt; +use futures_util::{future::IntoStream, FutureExt}; use http::header::HeaderValue; -use http::uri::InvalidUri; -use hyper::client::{HttpConnector, ResponseFuture}; -use hyper::header::USER_AGENT; -use hyper::{Body, Client, Request, Response, StatusCode}; +use hyper::{ + client::{HttpConnector, ResponseFuture}, + header::USER_AGENT, + Body, Client, Request, Response, StatusCode, +}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::HttpsConnector; use rustls::{ClientConfig, RootCertStore}; use thiserror::Error; use url::Url; -use std::env::consts::OS; - -use crate::version::{ - FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING, +use crate::{ + version::{FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING}, + Error, }; +#[derive(Debug, Error)] +pub enum HttpClientError { + #[error("Response status code: {0}")] + StatusCode(hyper::StatusCode), +} + +impl From for Error { + fn from(err: HttpClientError) -> Self { + match err { + HttpClientError::StatusCode(code) => { + // not exhaustive, but what reasonably could be expected + match code { + StatusCode::GATEWAY_TIMEOUT | StatusCode::REQUEST_TIMEOUT => { + Error::deadline_exceeded(err) + } + StatusCode::GONE + | StatusCode::NOT_FOUND + | StatusCode::MOVED_PERMANENTLY + | StatusCode::PERMANENT_REDIRECT + | StatusCode::TEMPORARY_REDIRECT => Error::not_found(err), + StatusCode::FORBIDDEN | StatusCode::PAYMENT_REQUIRED => { + Error::permission_denied(err) + } + StatusCode::NETWORK_AUTHENTICATION_REQUIRED + | StatusCode::PROXY_AUTHENTICATION_REQUIRED + | StatusCode::UNAUTHORIZED => Error::unauthenticated(err), + StatusCode::EXPECTATION_FAILED + | StatusCode::PRECONDITION_FAILED + | StatusCode::PRECONDITION_REQUIRED => Error::failed_precondition(err), + StatusCode::RANGE_NOT_SATISFIABLE => Error::out_of_range(err), + StatusCode::INTERNAL_SERVER_ERROR + | StatusCode::MISDIRECTED_REQUEST + | StatusCode::SERVICE_UNAVAILABLE + | StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => Error::unavailable(err), + StatusCode::BAD_REQUEST + | StatusCode::HTTP_VERSION_NOT_SUPPORTED + | StatusCode::LENGTH_REQUIRED + | StatusCode::METHOD_NOT_ALLOWED + | StatusCode::NOT_ACCEPTABLE + | StatusCode::PAYLOAD_TOO_LARGE + | StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE + | StatusCode::UNSUPPORTED_MEDIA_TYPE + | StatusCode::URI_TOO_LONG => Error::invalid_argument(err), + StatusCode::TOO_MANY_REQUESTS => Error::resource_exhausted(err), + StatusCode::NOT_IMPLEMENTED => Error::unimplemented(err), + _ => Error::unknown(err), + } + } + } + } +} + pub struct HttpClient { user_agent: HeaderValue, proxy: Option, tls_config: ClientConfig, } -#[derive(Error, Debug)] -pub enum HttpClientError { - #[error("could not parse request: {0}")] - Parsing(#[from] http::Error), - #[error("could not send request: {0}")] - Request(hyper::Error), - #[error("could not read response: {0}")] - Response(hyper::Error), - #[error("status code: {0}")] - NotOK(u16), - #[error("could not build proxy connector: {0}")] - ProxyBuilder(#[from] std::io::Error), -} - -impl From for HttpClientError { - fn from(err: InvalidUri) -> Self { - Self::Parsing(err.into()) - } -} - impl HttpClient { pub fn new(proxy: Option<&Url>) -> Self { let spotify_version = match OS { @@ -53,7 +86,7 @@ impl HttpClient { let spotify_platform = match OS { "android" => "Android/31", - "ios" => "iOS/15.1.1", + "ios" => "iOS/15.2", "macos" => "OSX/0", "windows" => "Win32/0", _ => "Linux/0", @@ -95,37 +128,32 @@ impl HttpClient { } } - pub async fn request(&self, req: Request) -> Result, HttpClientError> { + pub async fn request(&self, req: Request) -> Result, Error> { debug!("Requesting {:?}", req.uri().to_string()); let request = self.request_fut(req)?; - { - let response = request.await; - if let Ok(response) = &response { - let status = response.status(); - if status != StatusCode::OK { - return Err(HttpClientError::NotOK(status.into())); - } + let response = request.await; + + if let Ok(response) = &response { + let code = response.status(); + if code != StatusCode::OK { + return Err(HttpClientError::StatusCode(code).into()); } - response.map_err(HttpClientError::Response) } + + Ok(response?) } - pub async fn request_body(&self, req: Request) -> Result { + pub async fn request_body(&self, req: Request) -> Result { let response = self.request(req).await?; - hyper::body::to_bytes(response.into_body()) - .await - .map_err(HttpClientError::Response) + Ok(hyper::body::to_bytes(response.into_body()).await?) } - pub fn request_stream( - &self, - req: Request, - ) -> Result, HttpClientError> { + pub fn request_stream(&self, req: Request) -> Result, Error> { Ok(self.request_fut(req)?.into_stream()) } - pub fn request_fut(&self, mut req: Request) -> Result { + pub fn request_fut(&self, mut req: Request) -> Result { let mut http = HttpConnector::new(); http.enforce_http(false); let connector = HttpsConnector::from((http, self.tls_config.clone())); diff --git a/core/src/lib.rs b/core/src/lib.rs index 76ddbd37..a0f180ca 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -20,6 +20,7 @@ pub mod date; mod dealer; #[doc(hidden)] pub mod diffie_hellman; +pub mod error; pub mod file_id; mod http_client; pub mod mercury; @@ -34,3 +35,9 @@ pub mod token; #[doc(hidden)] pub mod util; pub mod version; + +pub use config::SessionConfig; +pub use error::Error; +pub use file_id::FileId; +pub use session::Session; +pub use spotify_id::SpotifyId; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index ad2d5013..b693444a 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -1,9 +1,10 @@ -use std::collections::HashMap; -use std::future::Future; -use std::mem; -use std::pin::Pin; -use std::task::Context; -use std::task::Poll; +use std::{ + collections::HashMap, + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, +}; use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; @@ -11,9 +12,7 @@ use futures_util::FutureExt; use protobuf::Message; use tokio::sync::{mpsc, oneshot}; -use crate::packet::PacketType; -use crate::protocol; -use crate::util::SeqGenerator; +use crate::{packet::PacketType, protocol, util::SeqGenerator, Error}; mod types; pub use self::types::*; @@ -33,18 +32,18 @@ component! { pub struct MercuryPending { parts: Vec>, partial: Option>, - callback: Option>>, + callback: Option>>, } pub struct MercuryFuture { - receiver: oneshot::Receiver>, + receiver: oneshot::Receiver>, } impl Future for MercuryFuture { - type Output = Result; + type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - self.receiver.poll_unpin(cx).map_err(|_| MercuryError)? + self.receiver.poll_unpin(cx)? } } @@ -55,7 +54,7 @@ impl MercuryManager { seq } - fn request(&self, req: MercuryRequest) -> MercuryFuture { + fn request(&self, req: MercuryRequest) -> Result, Error> { let (tx, rx) = oneshot::channel(); let pending = MercuryPending { @@ -72,13 +71,13 @@ impl MercuryManager { }); let cmd = req.method.command(); - let data = req.encode(&seq); + let data = req.encode(&seq)?; - self.session().send_packet(cmd, data); - MercuryFuture { receiver: rx } + self.session().send_packet(cmd, data)?; + Ok(MercuryFuture { receiver: rx }) } - pub fn get>(&self, uri: T) -> MercuryFuture { + pub fn get>(&self, uri: T) -> Result, Error> { self.request(MercuryRequest { method: MercuryMethod::Get, uri: uri.into(), @@ -87,7 +86,11 @@ impl MercuryManager { }) } - pub fn send>(&self, uri: T, data: Vec) -> MercuryFuture { + pub fn send>( + &self, + uri: T, + data: Vec, + ) -> Result, Error> { self.request(MercuryRequest { method: MercuryMethod::Send, uri: uri.into(), @@ -103,7 +106,7 @@ impl MercuryManager { pub fn subscribe>( &self, uri: T, - ) -> impl Future, MercuryError>> + 'static + ) -> impl Future, Error>> + 'static { let uri = uri.into(); let request = self.request(MercuryRequest { @@ -115,7 +118,7 @@ impl MercuryManager { let manager = self.clone(); async move { - let response = request.await?; + let response = request?.await?; let (tx, rx) = mpsc::unbounded_channel(); @@ -125,13 +128,18 @@ impl MercuryManager { if !response.payload.is_empty() { // Old subscription protocol, watch the provided list of URIs for sub in response.payload { - let mut sub = - protocol::pubsub::Subscription::parse_from_bytes(&sub).unwrap(); - let sub_uri = sub.take_uri(); + match protocol::pubsub::Subscription::parse_from_bytes(&sub) { + Ok(mut sub) => { + let sub_uri = sub.take_uri(); - debug!("subscribed sub_uri={}", sub_uri); + debug!("subscribed sub_uri={}", sub_uri); - inner.subscriptions.push((sub_uri, tx.clone())); + inner.subscriptions.push((sub_uri, tx.clone())); + } + Err(e) => { + error!("could not subscribe to {}: {}", uri, e); + } + } } } else { // New subscription protocol, watch the requested URI @@ -165,7 +173,7 @@ impl MercuryManager { } } - pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) -> Result<(), Error> { let seq_len = BigEndian::read_u16(data.split_to(2).as_ref()) as usize; let seq = data.split_to(seq_len).as_ref().to_owned(); @@ -185,7 +193,7 @@ impl MercuryManager { } } else { warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8); - return; + return Err(MercuryError::Command(cmd).into()); } } }; @@ -205,10 +213,12 @@ impl MercuryManager { } if flags == 0x1 { - self.complete_request(cmd, pending); + self.complete_request(cmd, pending)?; } else { self.lock(move |inner| inner.pending.insert(seq, pending)); } + + Ok(()) } fn parse_part(data: &mut Bytes) -> Vec { @@ -216,9 +226,9 @@ impl MercuryManager { data.split_to(size).as_ref().to_owned() } - fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) { + fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) -> Result<(), Error> { let header_data = pending.parts.remove(0); - let header = protocol::mercury::Header::parse_from_bytes(&header_data).unwrap(); + let header = protocol::mercury::Header::parse_from_bytes(&header_data)?; let response = MercuryResponse { uri: header.get_uri().to_string(), @@ -226,13 +236,17 @@ impl MercuryManager { payload: pending.parts, }; - if response.status_code >= 500 { - panic!("Spotify servers returned an error. Restart librespot."); - } else if response.status_code >= 400 { - warn!("error {} for uri {}", response.status_code, &response.uri); + let status_code = response.status_code; + if status_code >= 500 { + error!("error {} for uri {}", status_code, &response.uri); + Err(MercuryError::Response(response).into()) + } else if status_code >= 400 { + error!("error {} for uri {}", status_code, &response.uri); if let Some(cb) = pending.callback { - let _ = cb.send(Err(MercuryError)); + cb.send(Err(MercuryError::Response(response.clone()).into())) + .map_err(|_| MercuryError::Channel)?; } + Err(MercuryError::Response(response).into()) } else if let PacketType::MercuryEvent = cmd { self.lock(|inner| { let mut found = false; @@ -242,7 +256,7 @@ impl MercuryManager { // before sending while saving the subscription under its unencoded form. let mut uri_split = response.uri.split('/'); - let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string()) + let encoded_uri = std::iter::once(uri_split.next().unwrap_or_default().to_string()) .chain(uri_split.map(|component| { form_urlencoded::byte_serialize(component.as_bytes()).collect::() })) @@ -263,12 +277,19 @@ impl MercuryManager { }); if !found { - debug!("unknown subscription uri={}", response.uri); + debug!("unknown subscription uri={}", &response.uri); trace!("response pushed over Mercury: {:?}", response); + Err(MercuryError::Response(response).into()) + } else { + Ok(()) } }) } else if let Some(cb) = pending.callback { - let _ = cb.send(Ok(response)); + cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?; + Ok(()) + } else { + error!("can't handle Mercury response: {:?}", response); + Err(MercuryError::Response(response).into()) } } diff --git a/core/src/mercury/sender.rs b/core/src/mercury/sender.rs index 268554d9..31409e88 100644 --- a/core/src/mercury/sender.rs +++ b/core/src/mercury/sender.rs @@ -1,6 +1,8 @@ use std::collections::VecDeque; -use super::*; +use super::{MercuryFuture, MercuryManager, MercuryResponse}; + +use crate::Error; pub struct MercurySender { mercury: MercuryManager, @@ -23,12 +25,13 @@ impl MercurySender { self.buffered_future.is_none() && self.pending.is_empty() } - pub fn send(&mut self, item: Vec) { - let task = self.mercury.send(self.uri.clone(), item); + pub fn send(&mut self, item: Vec) -> Result<(), Error> { + let task = self.mercury.send(self.uri.clone(), item)?; self.pending.push_back(task); + Ok(()) } - pub async fn flush(&mut self) -> Result<(), MercuryError> { + pub async fn flush(&mut self) -> Result<(), Error> { if self.buffered_future.is_none() { self.buffered_future = self.pending.pop_front(); } diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 007ffb38..9c7593fe 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -1,11 +1,10 @@ +use std::io::Write; + use byteorder::{BigEndian, WriteBytesExt}; use protobuf::Message; -use std::fmt; -use std::io::Write; use thiserror::Error; -use crate::packet::PacketType; -use crate::protocol; +use crate::{packet::PacketType, protocol, Error}; #[derive(Debug, PartialEq, Eq)] pub enum MercuryMethod { @@ -30,12 +29,23 @@ pub struct MercuryResponse { pub payload: Vec>, } -#[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)] -pub struct MercuryError; +#[derive(Debug, Error)] +pub enum MercuryError { + #[error("callback receiver was disconnected")] + Channel, + #[error("error handling packet type: {0:?}")] + Command(PacketType), + #[error("error handling Mercury response: {0:?}")] + Response(MercuryResponse), +} -impl fmt::Display for MercuryError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Mercury error") +impl From for Error { + fn from(err: MercuryError) -> Self { + match err { + MercuryError::Channel => Error::aborted(err), + MercuryError::Command(_) => Error::unimplemented(err), + MercuryError::Response(_) => Error::unavailable(err), + } } } @@ -63,15 +73,12 @@ impl MercuryMethod { } impl MercuryRequest { - // TODO: change into Result and remove unwraps - pub fn encode(&self, seq: &[u8]) -> Vec { + pub fn encode(&self, seq: &[u8]) -> Result, Error> { let mut packet = Vec::new(); - packet.write_u16::(seq.len() as u16).unwrap(); - packet.write_all(seq).unwrap(); - packet.write_u8(1).unwrap(); // Flags: FINAL - packet - .write_u16::(1 + self.payload.len() as u16) - .unwrap(); // Part count + packet.write_u16::(seq.len() as u16)?; + packet.write_all(seq)?; + packet.write_u8(1)?; // Flags: FINAL + packet.write_u16::(1 + self.payload.len() as u16)?; // Part count let mut header = protocol::mercury::Header::new(); header.set_uri(self.uri.clone()); @@ -81,16 +88,14 @@ impl MercuryRequest { header.set_content_type(content_type.clone()); } - packet - .write_u16::(header.compute_size() as u16) - .unwrap(); - header.write_to_writer(&mut packet).unwrap(); + packet.write_u16::(header.compute_size() as u16)?; + header.write_to_writer(&mut packet)?; for p in &self.payload { - packet.write_u16::(p.len() as u16).unwrap(); - packet.write_all(p).unwrap(); + packet.write_u16::(p.len() as u16)?; + packet.write_all(p)?; } - packet + Ok(packet) } } diff --git a/core/src/packet.rs b/core/src/packet.rs index de780f13..2f50d158 100644 --- a/core/src/packet.rs +++ b/core/src/packet.rs @@ -2,7 +2,7 @@ use num_derive::{FromPrimitive, ToPrimitive}; -#[derive(Debug, FromPrimitive, ToPrimitive)] +#[derive(Debug, Copy, Clone, FromPrimitive, ToPrimitive)] pub enum PacketType { SecretBlock = 0x02, Ping = 0x04, diff --git a/core/src/session.rs b/core/src/session.rs index 426480f6..72805551 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -1,13 +1,16 @@ -use std::collections::HashMap; -use std::future::Future; -use std::io; -use std::pin::Pin; -use std::process::exit; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, RwLock, Weak}; -use std::task::Context; -use std::task::Poll; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + collections::HashMap, + future::Future, + io, + pin::Pin, + process::exit, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, RwLock, Weak, + }, + task::{Context, Poll}, + time::{SystemTime, UNIX_EPOCH}, +}; use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; @@ -20,18 +23,21 @@ use thiserror::Error; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; -use crate::apresolve::ApResolver; -use crate::audio_key::AudioKeyManager; -use crate::authentication::Credentials; -use crate::cache::Cache; -use crate::channel::ChannelManager; -use crate::config::SessionConfig; -use crate::connection::{self, AuthenticationError}; -use crate::http_client::HttpClient; -use crate::mercury::MercuryManager; -use crate::packet::PacketType; -use crate::spclient::SpClient; -use crate::token::TokenProvider; +use crate::{ + apresolve::ApResolver, + audio_key::AudioKeyManager, + authentication::Credentials, + cache::Cache, + channel::ChannelManager, + config::SessionConfig, + connection::{self, AuthenticationError}, + http_client::HttpClient, + mercury::MercuryManager, + packet::PacketType, + spclient::SpClient, + token::TokenProvider, + Error, +}; #[derive(Debug, Error)] pub enum SessionError { @@ -39,6 +45,18 @@ pub enum SessionError { AuthenticationError(#[from] AuthenticationError), #[error("Cannot create session: {0}")] IoError(#[from] io::Error), + #[error("packet {0} unknown")] + Packet(u8), +} + +impl From for Error { + fn from(err: SessionError) -> Self { + match err { + SessionError::AuthenticationError(_) => Error::unauthenticated(err), + SessionError::IoError(_) => Error::unavailable(err), + SessionError::Packet(_) => Error::unimplemented(err), + } + } } pub type UserAttributes = HashMap; @@ -88,7 +106,7 @@ impl Session { config: SessionConfig, credentials: Credentials, cache: Option, - ) -> Result { + ) -> Result { let http_client = HttpClient::new(config.proxy.as_ref()); let (sender_tx, sender_rx) = mpsc::unbounded_channel(); let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); @@ -214,9 +232,18 @@ impl Session { } } - fn dispatch(&self, cmd: u8, data: Bytes) { + fn dispatch(&self, cmd: u8, data: Bytes) -> Result<(), Error> { use PacketType::*; + let packet_type = FromPrimitive::from_u8(cmd); + let cmd = match packet_type { + Some(cmd) => cmd, + None => { + trace!("Ignoring unknown packet {:x}", cmd); + return Err(SessionError::Packet(cmd).into()); + } + }; + match packet_type { Some(Ping) => { let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64; @@ -229,24 +256,21 @@ impl Session { self.0.data.write().unwrap().time_delta = server_timestamp - timestamp; self.debug_info(); - self.send_packet(Pong, vec![0, 0, 0, 0]); + self.send_packet(Pong, vec![0, 0, 0, 0]) } Some(CountryCode) => { - let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); + let country = String::from_utf8(data.as_ref().to_owned())?; info!("Country: {:?}", country); self.0.data.write().unwrap().user_data.country = country; + Ok(()) } - Some(StreamChunkRes) | Some(ChannelError) => { - self.channel().dispatch(packet_type.unwrap(), data); - } - Some(AesKey) | Some(AesKeyError) => { - self.audio_key().dispatch(packet_type.unwrap(), data); - } + Some(StreamChunkRes) | Some(ChannelError) => self.channel().dispatch(cmd, data), + Some(AesKey) | Some(AesKeyError) => self.audio_key().dispatch(cmd, data), Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => { - self.mercury().dispatch(packet_type.unwrap(), data); + self.mercury().dispatch(cmd, data) } Some(ProductInfo) => { - let data = std::str::from_utf8(&data).unwrap(); + let data = std::str::from_utf8(&data)?; let mut reader = quick_xml::Reader::from_str(data); let mut buf = Vec::new(); @@ -256,8 +280,7 @@ impl Session { loop { match reader.read_event(&mut buf) { Ok(Event::Start(ref element)) => { - current_element = - std::str::from_utf8(element.name()).unwrap().to_owned() + current_element = std::str::from_utf8(element.name())?.to_owned() } Ok(Event::End(_)) => { current_element = String::new(); @@ -266,7 +289,7 @@ impl Session { if !current_element.is_empty() { let _ = user_attributes.insert( current_element.clone(), - value.unescape_and_decode(&reader).unwrap(), + value.unescape_and_decode(&reader)?, ); } } @@ -284,24 +307,23 @@ impl Session { Self::check_catalogue(&user_attributes); self.0.data.write().unwrap().user_data.attributes = user_attributes; + Ok(()) } Some(PongAck) | Some(SecretBlock) | Some(LegacyWelcome) | Some(UnknownDataAllZeros) - | Some(LicenseVersion) => {} + | Some(LicenseVersion) => Ok(()), _ => { - if let Some(packet_type) = PacketType::from_u8(cmd) { - trace!("Ignoring {:?} packet with data {:#?}", packet_type, data); - } else { - trace!("Ignoring unknown packet {:x}", cmd); - } + trace!("Ignoring {:?} packet with data {:#?}", cmd, data); + Err(SessionError::Packet(cmd as u8).into()) } } } - pub fn send_packet(&self, cmd: PacketType, data: Vec) { - self.0.tx_connection.send((cmd as u8, data)).unwrap(); + pub fn send_packet(&self, cmd: PacketType, data: Vec) -> Result<(), Error> { + self.0.tx_connection.send((cmd as u8, data))?; + Ok(()) } pub fn cache(&self) -> Option<&Arc> { @@ -393,7 +415,7 @@ impl SessionWeak { } pub(crate) fn upgrade(&self) -> Session { - self.try_upgrade().expect("Session died") + self.try_upgrade().expect("Session died") // TODO } } @@ -434,7 +456,9 @@ where } }; - session.dispatch(cmd, data); + if let Err(e) = session.dispatch(cmd, data) { + error!("could not dispatch command: {}", e); + } } } } diff --git a/core/src/socket.rs b/core/src/socket.rs index 92274cc6..84ac6024 100644 --- a/core/src/socket.rs +++ b/core/src/socket.rs @@ -1,5 +1,4 @@ -use std::io; -use std::net::ToSocketAddrs; +use std::{io, net::ToSocketAddrs}; use tokio::net::TcpStream; use url::Url; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index c0336690..c4285cd4 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -1,22 +1,25 @@ -use crate::apresolve::SocketAddress; -use crate::file_id::FileId; -use crate::http_client::HttpClientError; -use crate::mercury::MercuryError; -use crate::protocol::canvaz::EntityCanvazRequest; -use crate::protocol::connect::PutStateRequest; -use crate::protocol::extended_metadata::BatchedEntityRequest; -use crate::spotify_id::SpotifyId; +use std::time::Duration; use bytes::Bytes; use futures_util::future::IntoStream; use http::header::HeaderValue; -use hyper::client::ResponseFuture; -use hyper::header::{InvalidHeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}; -use hyper::{Body, HeaderMap, Method, Request}; +use hyper::{ + client::ResponseFuture, + header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, + Body, HeaderMap, Method, Request, +}; use protobuf::Message; use rand::Rng; -use std::time::Duration; -use thiserror::Error; + +use crate::{ + apresolve::SocketAddress, + error::ErrorKind, + protocol::{ + canvaz::EntityCanvazRequest, connect::PutStateRequest, + extended_metadata::BatchedEntityRequest, + }, + Error, FileId, SpotifyId, +}; component! { SpClient : SpClientInner { @@ -25,23 +28,7 @@ component! { } } -pub type SpClientResult = Result; - -#[derive(Error, Debug)] -pub enum SpClientError { - #[error("could not get authorization token")] - Token(#[from] MercuryError), - #[error("could not parse request: {0}")] - Parsing(#[from] http::Error), - #[error("could not complete request: {0}")] - Network(#[from] HttpClientError), -} - -impl From for SpClientError { - fn from(err: InvalidHeaderValue) -> Self { - Self::Parsing(err.into()) - } -} +pub type SpClientResult = Result; #[derive(Copy, Clone, Debug)] pub enum RequestStrategy { @@ -157,12 +144,8 @@ impl SpClient { ))?, ); - last_response = self - .session() - .http_client() - .request_body(request) - .await - .map_err(SpClientError::Network); + last_response = self.session().http_client().request_body(request).await; + if last_response.is_ok() { return last_response; } @@ -177,9 +160,9 @@ impl SpClient { // Reconnection logic: drop the current access point if we are experiencing issues. // This will cause the next call to base_url() to resolve a new one. - if let Err(SpClientError::Network(ref network_error)) = last_response { - match network_error { - HttpClientError::Response(_) | HttpClientError::Request(_) => { + if let Err(ref network_error) = last_response { + match network_error.kind { + ErrorKind::Unavailable | ErrorKind::DeadlineExceeded => { // Keep trying the current access point three times before dropping it. if tries % 3 == 0 { self.flush_accesspoint().await @@ -244,7 +227,7 @@ impl SpClient { } pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult { - let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62(),); + let endpoint = format!("/color-lyrics/v1/track/{}", track_id.to_base62()); self.request_as_json(&Method::GET, &endpoint, None, None) .await @@ -291,7 +274,7 @@ impl SpClient { url: &str, offset: usize, length: usize, - ) -> Result, SpClientError> { + ) -> Result, Error> { let req = Request::builder() .method(&Method::GET) .uri(url) diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 9f6d92ed..15b365b0 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -1,13 +1,17 @@ -use librespot_protocol as protocol; +use std::{ + convert::{TryFrom, TryInto}, + fmt, + ops::Deref, +}; use thiserror::Error; -use std::convert::{TryFrom, TryInto}; -use std::fmt; -use std::ops::Deref; +use crate::Error; + +use librespot_protocol as protocol; // re-export FileId for historic reasons, when it was part of this mod -pub use crate::file_id::FileId; +pub use crate::FileId; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SpotifyItemType { @@ -64,8 +68,14 @@ pub enum SpotifyIdError { InvalidRoot, } -pub type SpotifyIdResult = Result; -pub type NamedSpotifyIdResult = Result; +impl From for Error { + fn from(err: SpotifyIdError) -> Self { + Error::invalid_argument(err) + } +} + +pub type SpotifyIdResult = Result; +pub type NamedSpotifyIdResult = Result; const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef"; @@ -95,7 +105,7 @@ impl SpotifyId { let p = match c { b'0'..=b'9' => c - b'0', b'a'..=b'f' => c - b'a' + 10, - _ => return Err(SpotifyIdError::InvalidId), + _ => return Err(SpotifyIdError::InvalidId.into()), } as u128; dst <<= 4; @@ -121,7 +131,7 @@ impl SpotifyId { b'0'..=b'9' => c - b'0', b'a'..=b'z' => c - b'a' + 10, b'A'..=b'Z' => c - b'A' + 36, - _ => return Err(SpotifyIdError::InvalidId), + _ => return Err(SpotifyIdError::InvalidId.into()), } as u128; dst *= 62; @@ -143,7 +153,7 @@ impl SpotifyId { id: u128::from_be_bytes(dst), item_type: SpotifyItemType::Unknown, }), - Err(_) => Err(SpotifyIdError::InvalidId), + Err(_) => Err(SpotifyIdError::InvalidId.into()), } } @@ -161,20 +171,20 @@ impl SpotifyId { // At minimum, should be `spotify:{type}:{id}` if uri_parts.len() < 3 { - return Err(SpotifyIdError::InvalidFormat); + return Err(SpotifyIdError::InvalidFormat.into()); } if uri_parts[0] != "spotify" { - return Err(SpotifyIdError::InvalidRoot); + return Err(SpotifyIdError::InvalidRoot.into()); } - let id = uri_parts.pop().unwrap(); + let id = uri_parts.pop().unwrap_or_default(); if id.len() != Self::SIZE_BASE62 { - return Err(SpotifyIdError::InvalidId); + return Err(SpotifyIdError::InvalidId.into()); } Ok(Self { - item_type: uri_parts.pop().unwrap().into(), + item_type: uri_parts.pop().unwrap_or_default().into(), ..Self::from_base62(id)? }) } @@ -285,15 +295,15 @@ impl NamedSpotifyId { // At minimum, should be `spotify:user:{username}:{type}:{id}` if uri_parts.len() < 5 { - return Err(SpotifyIdError::InvalidFormat); + return Err(SpotifyIdError::InvalidFormat.into()); } if uri_parts[0] != "spotify" { - return Err(SpotifyIdError::InvalidRoot); + return Err(SpotifyIdError::InvalidRoot.into()); } if uri_parts[1] != "user" { - return Err(SpotifyIdError::InvalidFormat); + return Err(SpotifyIdError::InvalidFormat.into()); } Ok(Self { @@ -344,35 +354,35 @@ impl fmt::Display for NamedSpotifyId { } impl TryFrom<&[u8]> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(src: &[u8]) -> Result { Self::from_raw(src) } } impl TryFrom<&str> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(src: &str) -> Result { Self::from_base62(src) } } impl TryFrom for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(src: String) -> Result { Self::try_from(src.as_str()) } } impl TryFrom<&Vec> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(src: &Vec) -> Result { Self::try_from(src.as_slice()) } } impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(track: &protocol::spirc::TrackRef) -> Result { match SpotifyId::from_raw(track.get_gid()) { Ok(mut id) => { @@ -385,7 +395,7 @@ impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId { } impl TryFrom<&protocol::metadata::Album> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(album: &protocol::metadata::Album) -> Result { Ok(Self { item_type: SpotifyItemType::Album, @@ -395,7 +405,7 @@ impl TryFrom<&protocol::metadata::Album> for SpotifyId { } impl TryFrom<&protocol::metadata::Artist> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(artist: &protocol::metadata::Artist) -> Result { Ok(Self { item_type: SpotifyItemType::Artist, @@ -405,7 +415,7 @@ impl TryFrom<&protocol::metadata::Artist> for SpotifyId { } impl TryFrom<&protocol::metadata::Episode> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(episode: &protocol::metadata::Episode) -> Result { Ok(Self { item_type: SpotifyItemType::Episode, @@ -415,7 +425,7 @@ impl TryFrom<&protocol::metadata::Episode> for SpotifyId { } impl TryFrom<&protocol::metadata::Track> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(track: &protocol::metadata::Track) -> Result { Ok(Self { item_type: SpotifyItemType::Track, @@ -425,7 +435,7 @@ impl TryFrom<&protocol::metadata::Track> for SpotifyId { } impl TryFrom<&protocol::metadata::Show> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(show: &protocol::metadata::Show) -> Result { Ok(Self { item_type: SpotifyItemType::Show, @@ -435,7 +445,7 @@ impl TryFrom<&protocol::metadata::Show> for SpotifyId { } impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result { Ok(Self { item_type: SpotifyItemType::Artist, @@ -445,7 +455,7 @@ impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId { } impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(item: &protocol::playlist4_external::Item) -> Result { Ok(Self { item_type: SpotifyItemType::Track, @@ -457,7 +467,7 @@ impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId { // Note that this is the unique revision of an item's metadata on a playlist, // not the ID of that item or playlist. impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result { Self::try_from(item.get_revision()) } @@ -465,7 +475,7 @@ impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId { // Note that this is the unique revision of a playlist, not the ID of that playlist. impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from( playlist: &protocol::playlist4_external::SelectedListContent, ) -> Result { @@ -477,7 +487,7 @@ impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId { // which is why we now don't create a separate `Playlist` enum value yet and choose // to discard any item type. impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId { - type Error = SpotifyIdError; + type Error = crate::Error; fn try_from( picture: &protocol::playlist_annotate3::TranscodedPicture, ) -> Result { @@ -565,7 +575,7 @@ mod tests { id: 0, kind: SpotifyItemType::Unknown, // Invalid ID in the URI. - uri_error: Some(SpotifyIdError::InvalidId), + uri_error: SpotifyIdError::InvalidId, uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH", base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9", base62: "!!!!!Ys0csV6RS48xBl0tH", @@ -578,7 +588,7 @@ mod tests { id: 0, kind: SpotifyItemType::Unknown, // Missing colon between ID and type. - uri_error: Some(SpotifyIdError::InvalidFormat), + uri_error: SpotifyIdError::InvalidFormat, uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", base16: "--------------------", base62: "....................", @@ -591,7 +601,7 @@ mod tests { id: 0, kind: SpotifyItemType::Unknown, // Uri too short - uri_error: Some(SpotifyIdError::InvalidId), + uri_error: SpotifyIdError::InvalidId, uri: "spotify:azb:aRS48xBl0tH", base16: "--------------------", base62: "....................", diff --git a/core/src/token.rs b/core/src/token.rs index b9afa620..0c0b7394 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -8,12 +8,12 @@ // user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming, // app-remote-control -use crate::mercury::MercuryError; +use std::time::{Duration, Instant}; use serde::Deserialize; +use thiserror::Error; -use std::error::Error; -use std::time::{Duration, Instant}; +use crate::Error; component! { TokenProvider : TokenProviderInner { @@ -21,6 +21,18 @@ component! { } } +#[derive(Debug, Error)] +pub enum TokenError { + #[error("no tokens available")] + Empty, +} + +impl From for Error { + fn from(err: TokenError) -> Self { + Error::unavailable(err) + } +} + #[derive(Clone, Debug)] pub struct Token { pub access_token: String, @@ -54,11 +66,7 @@ impl TokenProvider { } // scopes must be comma-separated - pub async fn get_token(&self, scopes: &str) -> Result { - if scopes.is_empty() { - return Err(MercuryError); - } - + pub async fn get_token(&self, scopes: &str) -> Result { if let Some(index) = self.find_token(scopes.split(',').collect()) { let cached_token = self.lock(|inner| inner.tokens[index].clone()); if cached_token.is_expired() { @@ -79,14 +87,10 @@ impl TokenProvider { Self::KEYMASTER_CLIENT_ID, self.session().device_id() ); - let request = self.session().mercury().get(query_uri); + let request = self.session().mercury().get(query_uri)?; let response = request.await?; - let data = response - .payload - .first() - .expect("No tokens received") - .to_vec(); - let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?; + let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec(); + let token = Token::new(String::from_utf8(data)?)?; trace!("Got token: {:#?}", token); self.lock(|inner| inner.tokens.push(token.clone())); Ok(token) @@ -96,7 +100,7 @@ impl TokenProvider { impl Token { const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10); - pub fn new(body: String) -> Result> { + pub fn new(body: String) -> Result { let data: TokenData = serde_json::from_slice(body.as_ref())?; Ok(Self { access_token: data.access_token, diff --git a/core/src/util.rs b/core/src/util.rs index 4f78c467..a01f8b56 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,15 +1,13 @@ -use std::future::Future; -use std::mem; -use std::pin::Pin; -use std::task::Context; -use std::task::Poll; +use std::{ + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, +}; use futures_core::ready; -use futures_util::FutureExt; -use futures_util::Sink; -use futures_util::{future, SinkExt}; -use tokio::task::JoinHandle; -use tokio::time::timeout; +use futures_util::{future, FutureExt, Sink, SinkExt}; +use tokio::{task::JoinHandle, time::timeout}; /// Returns a future that will flush the sink, even if flushing is temporarily completed. /// Finishes only if the sink throws an error. diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 368f3747..7edd934a 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -13,6 +13,7 @@ base64 = "0.13" cfg-if = "1.0" form_urlencoded = "1.0" futures-core = "0.3" +futures-util = "0.3" hmac = "0.11" hyper = { version = "0.14", features = ["server", "http1", "tcp"] } libmdns = "0.6" diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index 98f776fb..a29b3b8c 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -27,6 +27,8 @@ pub use crate::core::authentication::Credentials; /// Determining the icon in the list of available devices. pub use crate::core::config::DeviceType; +pub use crate::core::Error; + /// Makes this device visible to Spotify clients in the local network. /// /// `Discovery` implements the [`Stream`] trait. Every time this device @@ -48,13 +50,28 @@ pub struct Builder { /// Errors that can occur while setting up a [`Discovery`] instance. #[derive(Debug, Error)] -pub enum Error { +pub enum DiscoveryError { /// Setting up service discovery via DNS-SD failed. #[error("Setting up dns-sd failed: {0}")] DnsSdError(#[from] io::Error), /// Setting up the http server failed. + #[error("Creating SHA1 HMAC failed for base key {0:?}")] + HmacError(Vec), #[error("Setting up the http server failed: {0}")] HttpServerError(#[from] hyper::Error), + #[error("Missing params for key {0}")] + ParamsError(&'static str), +} + +impl From for Error { + fn from(err: DiscoveryError) -> Self { + match err { + DiscoveryError::DnsSdError(_) => Error::unavailable(err), + DiscoveryError::HmacError(_) => Error::invalid_argument(err), + DiscoveryError::HttpServerError(_) => Error::unavailable(err), + DiscoveryError::ParamsError(_) => Error::invalid_argument(err), + } + } } impl Builder { @@ -96,7 +113,7 @@ impl Builder { pub fn launch(self) -> Result { let mut port = self.port; let name = self.server_config.name.clone().into_owned(); - let server = DiscoveryServer::new(self.server_config, &mut port)?; + let server = DiscoveryServer::new(self.server_config, &mut port)??; let svc; @@ -109,8 +126,7 @@ impl Builder { None, port, &["VERSION=1.0", "CPath=/"], - ) - .unwrap(); + )?; } else { let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; diff --git a/discovery/src/server.rs b/discovery/src/server.rs index a82f90c0..74af6fa3 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -1,26 +1,35 @@ -use std::borrow::Cow; -use std::collections::BTreeMap; -use std::convert::Infallible; -use std::net::{Ipv4Addr, SocketAddr}; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; +use std::{ + borrow::Cow, + collections::BTreeMap, + convert::Infallible, + net::{Ipv4Addr, SocketAddr}, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; -use aes_ctr::cipher::generic_array::GenericArray; -use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; -use aes_ctr::Aes128Ctr; +use aes_ctr::{ + cipher::generic_array::GenericArray, + cipher::{NewStreamCipher, SyncStreamCipher}, + Aes128Ctr, +}; use futures_core::Stream; +use futures_util::{FutureExt, TryFutureExt}; use hmac::{Hmac, Mac, NewMac}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Method, Request, Response, StatusCode}; -use log::{debug, warn}; +use hyper::{ + service::{make_service_fn, service_fn}, + Body, Method, Request, Response, StatusCode, +}; +use log::{debug, error, warn}; use serde_json::json; use sha1::{Digest, Sha1}; use tokio::sync::{mpsc, oneshot}; -use crate::core::authentication::Credentials; -use crate::core::config::DeviceType; -use crate::core::diffie_hellman::DhLocalKeys; +use super::DiscoveryError; + +use crate::core::{ + authentication::Credentials, config::DeviceType, diffie_hellman::DhLocalKeys, Error, +}; type Params<'a> = BTreeMap, Cow<'a, str>>; @@ -76,14 +85,26 @@ impl RequestHandler { Response::new(Body::from(body)) } - fn handle_add_user(&self, params: &Params<'_>) -> Response { - let username = params.get("userName").unwrap().as_ref(); - let encrypted_blob = params.get("blob").unwrap(); - let client_key = params.get("clientKey").unwrap(); + fn handle_add_user(&self, params: &Params<'_>) -> Result, Error> { + let username_key = "userName"; + let username = params + .get(username_key) + .ok_or(DiscoveryError::ParamsError(username_key))? + .as_ref(); - let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); + let blob_key = "blob"; + let encrypted_blob = params + .get(blob_key) + .ok_or(DiscoveryError::ParamsError(blob_key))?; - let client_key = base64::decode(client_key.as_bytes()).unwrap(); + let clientkey_key = "clientKey"; + let client_key = params + .get(clientkey_key) + .ok_or(DiscoveryError::ParamsError(clientkey_key))?; + + let encrypted_blob = base64::decode(encrypted_blob.as_bytes())?; + + let client_key = base64::decode(client_key.as_bytes())?; let shared_key = self.keys.shared_secret(&client_key); let iv = &encrypted_blob[0..16]; @@ -94,21 +115,21 @@ impl RequestHandler { let base_key = &base_key[..16]; let checksum_key = { - let mut h = - Hmac::::new_from_slice(base_key).expect("HMAC can take key of any size"); + let mut h = Hmac::::new_from_slice(base_key) + .map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?; h.update(b"checksum"); h.finalize().into_bytes() }; let encryption_key = { - let mut h = - Hmac::::new_from_slice(base_key).expect("HMAC can take key of any size"); + let mut h = Hmac::::new_from_slice(base_key) + .map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?; h.update(b"encryption"); h.finalize().into_bytes() }; - let mut h = - Hmac::::new_from_slice(&checksum_key).expect("HMAC can take key of any size"); + let mut h = Hmac::::new_from_slice(&checksum_key) + .map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?; h.update(encrypted); if h.verify(cksum).is_err() { warn!("Login error for user {:?}: MAC mismatch", username); @@ -119,7 +140,7 @@ impl RequestHandler { }); let body = result.to_string(); - return Response::new(Body::from(body)); + return Ok(Response::new(Body::from(body))); } let decrypted = { @@ -132,9 +153,9 @@ impl RequestHandler { data }; - let credentials = Credentials::with_blob(username, &decrypted, &self.config.device_id); + let credentials = Credentials::with_blob(username, &decrypted, &self.config.device_id)?; - self.tx.send(credentials).unwrap(); + self.tx.send(credentials)?; let result = json!({ "status": 101, @@ -143,7 +164,7 @@ impl RequestHandler { }); let body = result.to_string(); - Response::new(Body::from(body)) + Ok(Response::new(Body::from(body))) } fn not_found(&self) -> Response { @@ -152,7 +173,10 @@ impl RequestHandler { res } - async fn handle(self: Arc, request: Request) -> hyper::Result> { + async fn handle( + self: Arc, + request: Request, + ) -> Result>, Error> { let mut params = Params::new(); let (parts, body) = request.into_parts(); @@ -172,11 +196,11 @@ impl RequestHandler { let action = params.get("action").map(Cow::as_ref); - Ok(match (parts.method, action) { + Ok(Ok(match (parts.method, action) { (Method::GET, Some("getInfo")) => self.handle_get_info(), - (Method::POST, Some("addUser")) => self.handle_add_user(¶ms), + (Method::POST, Some("addUser")) => self.handle_add_user(¶ms)?, _ => self.not_found(), - }) + })) } } @@ -186,7 +210,7 @@ pub struct DiscoveryServer { } impl DiscoveryServer { - pub fn new(config: Config, port: &mut u16) -> hyper::Result { + pub fn new(config: Config, port: &mut u16) -> Result, Error> { let (discovery, cred_rx) = RequestHandler::new(config); let discovery = Arc::new(discovery); @@ -197,7 +221,14 @@ impl DiscoveryServer { let make_service = make_service_fn(move |_| { let discovery = discovery.clone(); async move { - Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().handle(request))) + Ok::<_, hyper::Error>(service_fn(move |request| { + discovery + .clone() + .handle(request) + .inspect_err(|e| error!("could not handle discovery request: {}", e)) + .and_then(|x| async move { Ok(x) }) + .map(Result::unwrap) // guaranteed by `and_then` above + })) } }); @@ -209,8 +240,10 @@ impl DiscoveryServer { tokio::spawn(async { let result = server .with_graceful_shutdown(async { - close_rx.await.unwrap_err(); debug!("Shutting down discovery server"); + if close_rx.await.is_ok() { + debug!("unable to close discovery Rx channel completely"); + } }) .await; @@ -219,10 +252,10 @@ impl DiscoveryServer { } }); - Ok(Self { + Ok(Ok(Self { cred_rx, _close_tx: close_tx, - }) + })) } } diff --git a/metadata/src/album.rs b/metadata/src/album.rs index ac6fec20..6e07ed7e 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -1,30 +1,20 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; - -use crate::{ - artist::Artists, - availability::Availabilities, - copyright::Copyrights, - error::{MetadataError, RequestError}, - external_id::ExternalIds, - image::Images, - request::RequestResult, - restriction::Restrictions, - sale_period::SalePeriods, - track::Tracks, - util::try_from_repeated_message, - Metadata, +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, }; -use librespot_core::date::Date; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; +use crate::{ + artist::Artists, availability::Availabilities, copyright::Copyrights, external_id::ExternalIds, + image::Images, request::RequestResult, restriction::Restrictions, sale_period::SalePeriods, + track::Tracks, util::try_from_repeated_message, Metadata, +}; + +use librespot_core::{date::Date, Error, Session, SpotifyId}; + use librespot_protocol as protocol; - -use protocol::metadata::Disc as DiscMessage; - pub use protocol::metadata::Album_Type as AlbumType; +use protocol::metadata::Disc as DiscMessage; #[derive(Debug, Clone)] pub struct Album { @@ -94,20 +84,16 @@ impl Metadata for Album { type Message = protocol::metadata::Album; async fn request(session: &Session, album_id: SpotifyId) -> RequestResult { - session - .spclient() - .get_album_metadata(album_id) - .await - .map_err(RequestError::Http) + session.spclient().get_album_metadata(album_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Self::try_from(msg) } } impl TryFrom<&::Message> for Album { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(album: &::Message) -> Result { Ok(Self { id: album.try_into()?, @@ -138,7 +124,7 @@ impl TryFrom<&::Message> for Album { try_from_repeated_message!(::Message, Albums); impl TryFrom<&DiscMessage> for Disc { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(disc: &DiscMessage) -> Result { Ok(Self { number: disc.get_number(), diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index 517977bf..ac07d90e 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -1,23 +1,17 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; - -use crate::{ - error::{MetadataError, RequestError}, - request::RequestResult, - track::Tracks, - util::try_from_repeated_message, - Metadata, +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, }; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; +use crate::{request::RequestResult, track::Tracks, util::try_from_repeated_message, Metadata}; + +use librespot_core::{Error, Session, SpotifyId}; + use librespot_protocol as protocol; - use protocol::metadata::ArtistWithRole as ArtistWithRoleMessage; -use protocol::metadata::TopTracks as TopTracksMessage; - pub use protocol::metadata::ArtistWithRole_ArtistRole as ArtistRole; +use protocol::metadata::TopTracks as TopTracksMessage; #[derive(Debug, Clone)] pub struct Artist { @@ -88,20 +82,16 @@ impl Metadata for Artist { type Message = protocol::metadata::Artist; async fn request(session: &Session, artist_id: SpotifyId) -> RequestResult { - session - .spclient() - .get_artist_metadata(artist_id) - .await - .map_err(RequestError::Http) + session.spclient().get_artist_metadata(artist_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Self::try_from(msg) } } impl TryFrom<&::Message> for Artist { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(artist: &::Message) -> Result { Ok(Self { id: artist.try_into()?, @@ -114,7 +104,7 @@ impl TryFrom<&::Message> for Artist { try_from_repeated_message!(::Message, Artists); impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result { Ok(Self { id: artist_with_role.try_into()?, @@ -127,7 +117,7 @@ impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole { try_from_repeated_message!(ArtistWithRoleMessage, ArtistsWithRole); impl TryFrom<&TopTracksMessage> for TopTracks { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(top_tracks: &TopTracksMessage) -> Result { Ok(Self { country: top_tracks.get_country().to_owned(), diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs index fd202a40..d3ce69b7 100644 --- a/metadata/src/audio/file.rs +++ b/metadata/src/audio/file.rs @@ -1,12 +1,9 @@ -use std::collections::HashMap; -use std::fmt::Debug; -use std::ops::Deref; +use std::{collections::HashMap, fmt::Debug, ops::Deref}; + +use librespot_core::FileId; -use librespot_core::file_id::FileId; use librespot_protocol as protocol; - use protocol::metadata::AudioFile as AudioFileMessage; - pub use protocol::metadata::AudioFile_Format as AudioFileFormat; #[derive(Debug, Clone)] diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index 50aa2bf9..2b1f4eba 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -12,10 +12,9 @@ use crate::{ use super::file::AudioFiles; -use librespot_core::session::{Session, UserData}; -use librespot_core::spotify_id::{SpotifyId, SpotifyItemType}; +use librespot_core::{session::UserData, spotify_id::SpotifyItemType, Error, Session, SpotifyId}; -pub type AudioItemResult = Result; +pub type AudioItemResult = Result; // A wrapper with fields the player needs #[derive(Debug, Clone)] @@ -34,7 +33,7 @@ impl AudioItem { match id.item_type { SpotifyItemType::Track => Track::get_audio_item(session, id).await, SpotifyItemType::Episode => Episode::get_audio_item(session, id).await, - _ => Err(MetadataError::NonPlayable), + _ => Err(Error::unavailable(MetadataError::NonPlayable)), } } } diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index 27a85eed..d4681c28 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -1,13 +1,12 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use thiserror::Error; use crate::util::from_repeated_message; use librespot_core::date::Date; -use librespot_protocol as protocol; +use librespot_protocol as protocol; use protocol::metadata::Availability as AvailabilityMessage; pub type AudioItemAvailability = Result<(), UnavailabilityReason>; diff --git a/metadata/src/content_rating.rs b/metadata/src/content_rating.rs index a6f061d0..343f0e26 100644 --- a/metadata/src/content_rating.rs +++ b/metadata/src/content_rating.rs @@ -1,10 +1,8 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::util::from_repeated_message; use librespot_protocol as protocol; - use protocol::metadata::ContentRating as ContentRatingMessage; #[derive(Debug, Clone)] diff --git a/metadata/src/copyright.rs b/metadata/src/copyright.rs index 7842b7dd..b7f0e838 100644 --- a/metadata/src/copyright.rs +++ b/metadata/src/copyright.rs @@ -1,12 +1,9 @@ -use std::fmt::Debug; -use std::ops::Deref; - -use librespot_protocol as protocol; +use std::{fmt::Debug, ops::Deref}; use crate::util::from_repeated_message; +use librespot_protocol as protocol; use protocol::metadata::Copyright as CopyrightMessage; - pub use protocol::metadata::Copyright_Type as CopyrightType; #[derive(Debug, Clone)] diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 05d68aaf..30aae5a8 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -1,6 +1,8 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; use crate::{ audio::{ @@ -9,7 +11,6 @@ use crate::{ }, availability::Availabilities, content_rating::ContentRatings, - error::{MetadataError, RequestError}, image::Images, request::RequestResult, restriction::Restrictions, @@ -18,11 +19,9 @@ use crate::{ Metadata, }; -use librespot_core::date::Date; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; -use librespot_protocol as protocol; +use librespot_core::{date::Date, Error, Session, SpotifyId}; +use librespot_protocol as protocol; pub use protocol::metadata::Episode_EpisodeType as EpisodeType; #[derive(Debug, Clone)] @@ -90,20 +89,16 @@ impl Metadata for Episode { type Message = protocol::metadata::Episode; async fn request(session: &Session, episode_id: SpotifyId) -> RequestResult { - session - .spclient() - .get_episode_metadata(episode_id) - .await - .map_err(RequestError::Http) + session.spclient().get_episode_metadata(episode_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Self::try_from(msg) } } impl TryFrom<&::Message> for Episode { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(episode: &::Message) -> Result { Ok(Self { id: episode.try_into()?, diff --git a/metadata/src/error.rs b/metadata/src/error.rs index d1f6cc0b..31c600b0 100644 --- a/metadata/src/error.rs +++ b/metadata/src/error.rs @@ -1,35 +1,10 @@ use std::fmt::Debug; use thiserror::Error; -use protobuf::ProtobufError; - -use librespot_core::date::DateError; -use librespot_core::mercury::MercuryError; -use librespot_core::spclient::SpClientError; -use librespot_core::spotify_id::SpotifyIdError; - -#[derive(Debug, Error)] -pub enum RequestError { - #[error("could not get metadata over HTTP: {0}")] - Http(#[from] SpClientError), - #[error("could not get metadata over Mercury: {0}")] - Mercury(#[from] MercuryError), - #[error("response was empty")] - Empty, -} - #[derive(Debug, Error)] pub enum MetadataError { - #[error("{0}")] - InvalidSpotifyId(#[from] SpotifyIdError), - #[error("item has invalid date")] - InvalidTimestamp(#[from] DateError), - #[error("audio item is non-playable")] + #[error("empty response")] + Empty, + #[error("audio item is non-playable when it should be")] NonPlayable, - #[error("could not parse protobuf: {0}")] - Protobuf(#[from] ProtobufError), - #[error("error executing request: {0}")] - Request(#[from] RequestError), - #[error("could not parse repeated fields")] - InvalidRepeated, } diff --git a/metadata/src/external_id.rs b/metadata/src/external_id.rs index 5da45634..b310200a 100644 --- a/metadata/src/external_id.rs +++ b/metadata/src/external_id.rs @@ -1,10 +1,8 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::util::from_repeated_message; use librespot_protocol as protocol; - use protocol::metadata::ExternalId as ExternalIdMessage; #[derive(Debug, Clone)] diff --git a/metadata/src/image.rs b/metadata/src/image.rs index 345722c9..495158d6 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -1,22 +1,19 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; - -use crate::{ - error::MetadataError, - util::{from_repeated_message, try_from_repeated_message}, +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, }; -use librespot_core::file_id::FileId; -use librespot_core::spotify_id::SpotifyId; -use librespot_protocol as protocol; +use crate::util::{from_repeated_message, try_from_repeated_message}; +use librespot_core::{FileId, SpotifyId}; + +use librespot_protocol as protocol; use protocol::metadata::Image as ImageMessage; +pub use protocol::metadata::Image_Size as ImageSize; use protocol::playlist4_external::PictureSize as PictureSizeMessage; use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage; -pub use protocol::metadata::Image_Size as ImageSize; - #[derive(Debug, Clone)] pub struct Image { pub id: FileId, @@ -92,7 +89,7 @@ impl From<&PictureSizeMessage> for PictureSize { from_repeated_message!(PictureSizeMessage, PictureSizes); impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(picture: &TranscodedPictureMessage) -> Result { Ok(Self { target_name: picture.get_target_name().to_owned(), diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index af9c80ec..577af387 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -6,8 +6,7 @@ extern crate async_trait; use protobuf::Message; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; +use librespot_core::{Error, Session, SpotifyId}; pub mod album; pub mod artist; @@ -46,12 +45,12 @@ pub trait Metadata: Send + Sized + 'static { async fn request(session: &Session, id: SpotifyId) -> RequestResult; // Request a metadata struct - async fn get(session: &Session, id: SpotifyId) -> Result { + async fn get(session: &Session, id: SpotifyId) -> Result { let response = Self::request(session, id).await?; let msg = Self::Message::parse_from_bytes(&response)?; trace!("Received metadata: {:#?}", msg); Self::parse(&msg, id) } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result; + fn parse(msg: &Self::Message, _: SpotifyId) -> Result; } diff --git a/metadata/src/playlist/annotation.rs b/metadata/src/playlist/annotation.rs index 0116d997..587f9b39 100644 --- a/metadata/src/playlist/annotation.rs +++ b/metadata/src/playlist/annotation.rs @@ -4,16 +4,14 @@ use std::fmt::Debug; use protobuf::Message; use crate::{ - error::MetadataError, image::TranscodedPictures, request::{MercuryRequest, RequestResult}, Metadata, }; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; -use librespot_protocol as protocol; +use librespot_core::{Error, Session, SpotifyId}; +use librespot_protocol as protocol; pub use protocol::playlist_annotate3::AbuseReportState; #[derive(Debug, Clone)] @@ -34,7 +32,7 @@ impl Metadata for PlaylistAnnotation { Self::request_for_user(session, ¤t_user, playlist_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Ok(Self { description: msg.get_description().to_owned(), picture: msg.get_picture().to_owned(), // TODO: is this a URL or Spotify URI? @@ -64,7 +62,7 @@ impl PlaylistAnnotation { session: &Session, username: &str, playlist_id: SpotifyId, - ) -> Result { + ) -> Result { let response = Self::request_for_user(session, username, playlist_id).await?; let msg = ::Message::parse_from_bytes(&response)?; Self::parse(&msg, playlist_id) @@ -74,7 +72,7 @@ impl PlaylistAnnotation { impl MercuryRequest for PlaylistAnnotation {} impl TryFrom<&::Message> for PlaylistAnnotation { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from( annotation: &::Message, ) -> Result { diff --git a/metadata/src/playlist/attribute.rs b/metadata/src/playlist/attribute.rs index ac2eef65..eb4fb577 100644 --- a/metadata/src/playlist/attribute.rs +++ b/metadata/src/playlist/attribute.rs @@ -1,25 +1,25 @@ -use std::collections::HashMap; -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; -use crate::{error::MetadataError, image::PictureSizes, util::from_repeated_enum}; +use crate::{image::PictureSizes, util::from_repeated_enum}; + +use librespot_core::{date::Date, SpotifyId}; -use librespot_core::date::Date; -use librespot_core::spotify_id::SpotifyId; use librespot_protocol as protocol; - use protocol::playlist4_external::FormatListAttribute as PlaylistFormatAttributeMessage; +pub use protocol::playlist4_external::ItemAttributeKind as PlaylistItemAttributeKind; use protocol::playlist4_external::ItemAttributes as PlaylistItemAttributesMessage; use protocol::playlist4_external::ItemAttributesPartialState as PlaylistPartialItemAttributesMessage; +pub use protocol::playlist4_external::ListAttributeKind as PlaylistAttributeKind; use protocol::playlist4_external::ListAttributes as PlaylistAttributesMessage; use protocol::playlist4_external::ListAttributesPartialState as PlaylistPartialAttributesMessage; use protocol::playlist4_external::UpdateItemAttributes as PlaylistUpdateItemAttributesMessage; use protocol::playlist4_external::UpdateListAttributes as PlaylistUpdateAttributesMessage; -pub use protocol::playlist4_external::ItemAttributeKind as PlaylistItemAttributeKind; -pub use protocol::playlist4_external::ListAttributeKind as PlaylistAttributeKind; - #[derive(Debug, Clone)] pub struct PlaylistAttributes { pub name: String, @@ -108,7 +108,7 @@ pub struct PlaylistUpdateItemAttributes { } impl TryFrom<&PlaylistAttributesMessage> for PlaylistAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(attributes: &PlaylistAttributesMessage) -> Result { Ok(Self { name: attributes.get_name().to_owned(), @@ -142,7 +142,7 @@ impl From<&[PlaylistFormatAttributeMessage]> for PlaylistFormatAttribute { } impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result { Ok(Self { added_by: attributes.get_added_by().to_owned(), @@ -155,7 +155,7 @@ impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes { } } impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result { Ok(Self { values: attributes.get_values().try_into()?, @@ -165,7 +165,7 @@ impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes { } impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Result { Ok(Self { values: attributes.get_values().try_into()?, @@ -175,7 +175,7 @@ impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttri } impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result { Ok(Self { new_attributes: update.get_new_attributes().try_into()?, @@ -185,7 +185,7 @@ impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes { } impl TryFrom<&PlaylistUpdateItemAttributesMessage> for PlaylistUpdateItemAttributes { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(update: &PlaylistUpdateItemAttributesMessage) -> Result { Ok(Self { index: update.get_index(), diff --git a/metadata/src/playlist/diff.rs b/metadata/src/playlist/diff.rs index 080d72a1..4e40d2a5 100644 --- a/metadata/src/playlist/diff.rs +++ b/metadata/src/playlist/diff.rs @@ -1,13 +1,13 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; - -use crate::error::MetadataError; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, +}; use super::operation::PlaylistOperations; -use librespot_core::spotify_id::SpotifyId; -use librespot_protocol as protocol; +use librespot_core::SpotifyId; +use librespot_protocol as protocol; use protocol::playlist4_external::Diff as DiffMessage; #[derive(Debug, Clone)] @@ -18,7 +18,7 @@ pub struct PlaylistDiff { } impl TryFrom<&DiffMessage> for PlaylistDiff { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(diff: &DiffMessage) -> Result { Ok(Self { from_revision: diff.get_from_revision().try_into()?, diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index 5b97c382..dbd5fda2 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -1,17 +1,19 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; -use crate::{error::MetadataError, util::try_from_repeated_message}; +use crate::util::try_from_repeated_message; -use super::attribute::{PlaylistAttributes, PlaylistItemAttributes}; +use super::{ + attribute::{PlaylistAttributes, PlaylistItemAttributes}, + permission::Capabilities, +}; + +use librespot_core::{date::Date, SpotifyId}; -use librespot_core::date::Date; -use librespot_core::spotify_id::SpotifyId; use librespot_protocol as protocol; - -use super::permission::Capabilities; - use protocol::playlist4_external::Item as PlaylistItemMessage; use protocol::playlist4_external::ListItems as PlaylistItemsMessage; use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage; @@ -62,7 +64,7 @@ impl Deref for PlaylistMetaItems { } impl TryFrom<&PlaylistItemMessage> for PlaylistItem { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(item: &PlaylistItemMessage) -> Result { Ok(Self { id: item.try_into()?, @@ -74,7 +76,7 @@ impl TryFrom<&PlaylistItemMessage> for PlaylistItem { try_from_repeated_message!(PlaylistItemMessage, PlaylistItems); impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(list_items: &PlaylistItemsMessage) -> Result { Ok(Self { position: list_items.get_pos(), @@ -86,7 +88,7 @@ impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList { } impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(item: &PlaylistMetaItemMessage) -> Result { Ok(Self { revision: item.try_into()?, diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 5df839b1..612ef857 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -1,11 +1,12 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; use protobuf::Message; use crate::{ - error::MetadataError, request::{MercuryRequest, RequestResult}, util::{from_repeated_enum, try_from_repeated_message}, Metadata, @@ -16,11 +17,13 @@ use super::{ permission::Capabilities, }; -use librespot_core::date::Date; -use librespot_core::session::Session; -use librespot_core::spotify_id::{NamedSpotifyId, SpotifyId}; -use librespot_protocol as protocol; +use librespot_core::{ + date::Date, + spotify_id::{NamedSpotifyId, SpotifyId}, + Error, Session, +}; +use librespot_protocol as protocol; use protocol::playlist4_external::GeoblockBlockingType as Geoblock; #[derive(Debug, Clone)] @@ -111,7 +114,7 @@ impl Playlist { session: &Session, username: &str, playlist_id: SpotifyId, - ) -> Result { + ) -> Result { let response = Self::request_for_user(session, username, playlist_id).await?; let msg = ::Message::parse_from_bytes(&response)?; Self::parse(&msg, playlist_id) @@ -153,7 +156,7 @@ impl Metadata for Playlist { ::request(session, &uri).await } - fn parse(msg: &Self::Message, id: SpotifyId) -> Result { + fn parse(msg: &Self::Message, id: SpotifyId) -> Result { // the playlist proto doesn't contain the id so we decorate it let playlist = SelectedListContent::try_from(msg)?; let id = NamedSpotifyId::from_spotify_id(id, playlist.owner_username); @@ -188,10 +191,7 @@ impl RootPlaylist { } #[allow(dead_code)] - pub async fn get_root_for_user( - session: &Session, - username: &str, - ) -> Result { + pub async fn get_root_for_user(session: &Session, username: &str) -> Result { let response = Self::request_for_user(session, username).await?; let msg = protocol::playlist4_external::SelectedListContent::parse_from_bytes(&response)?; Ok(Self(SelectedListContent::try_from(&msg)?)) @@ -199,7 +199,7 @@ impl RootPlaylist { } impl TryFrom<&::Message> for SelectedListContent { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(playlist: &::Message) -> Result { Ok(Self { revision: playlist.get_revision().try_into()?, diff --git a/metadata/src/playlist/operation.rs b/metadata/src/playlist/operation.rs index c6ffa785..fe33d0dc 100644 --- a/metadata/src/playlist/operation.rs +++ b/metadata/src/playlist/operation.rs @@ -1,9 +1,10 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; use crate::{ - error::MetadataError, playlist::{ attribute::{PlaylistUpdateAttributes, PlaylistUpdateItemAttributes}, item::PlaylistItems, @@ -12,13 +13,11 @@ use crate::{ }; use librespot_protocol as protocol; - use protocol::playlist4_external::Add as PlaylistAddMessage; use protocol::playlist4_external::Mov as PlaylistMoveMessage; use protocol::playlist4_external::Op as PlaylistOperationMessage; -use protocol::playlist4_external::Rem as PlaylistRemoveMessage; - pub use protocol::playlist4_external::Op_Kind as PlaylistOperationKind; +use protocol::playlist4_external::Rem as PlaylistRemoveMessage; #[derive(Debug, Clone)] pub struct PlaylistOperation { @@ -64,7 +63,7 @@ pub struct PlaylistOperationRemove { } impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(operation: &PlaylistOperationMessage) -> Result { Ok(Self { kind: operation.get_kind(), @@ -80,7 +79,7 @@ impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation { try_from_repeated_message!(PlaylistOperationMessage, PlaylistOperations); impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(add: &PlaylistAddMessage) -> Result { Ok(Self { from_index: add.get_from_index(), @@ -102,7 +101,7 @@ impl From<&PlaylistMoveMessage> for PlaylistOperationMove { } impl TryFrom<&PlaylistRemoveMessage> for PlaylistOperationRemove { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(remove: &PlaylistRemoveMessage) -> Result { Ok(Self { from_index: remove.get_from_index(), diff --git a/metadata/src/playlist/permission.rs b/metadata/src/playlist/permission.rs index 163859a1..2923a636 100644 --- a/metadata/src/playlist/permission.rs +++ b/metadata/src/playlist/permission.rs @@ -1,10 +1,8 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::util::from_repeated_enum; use librespot_protocol as protocol; - use protocol::playlist_permission::Capabilities as CapabilitiesMessage; use protocol::playlist_permission::PermissionLevel; diff --git a/metadata/src/request.rs b/metadata/src/request.rs index 4e47fc38..2ebd4037 100644 --- a/metadata/src/request.rs +++ b/metadata/src/request.rs @@ -1,20 +1,21 @@ -use crate::error::RequestError; +use crate::MetadataError; -use librespot_core::session::Session; +use librespot_core::{Error, Session}; -pub type RequestResult = Result; +pub type RequestResult = Result; #[async_trait] pub trait MercuryRequest { async fn request(session: &Session, uri: &str) -> RequestResult { - let response = session.mercury().get(uri).await?; + let request = session.mercury().get(uri)?; + let response = request.await?; match response.payload.first() { Some(data) => { let data = data.to_vec().into(); trace!("Received metadata: {:?}", data); Ok(data) } - None => Err(RequestError::Empty), + None => Err(Error::unavailable(MetadataError::Empty)), } } } diff --git a/metadata/src/restriction.rs b/metadata/src/restriction.rs index 588e45e2..279da342 100644 --- a/metadata/src/restriction.rs +++ b/metadata/src/restriction.rs @@ -1,12 +1,10 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::util::{from_repeated_enum, from_repeated_message}; -use librespot_protocol as protocol; - use protocol::metadata::Restriction as RestrictionMessage; +use librespot_protocol as protocol; pub use protocol::metadata::Restriction_Catalogue as RestrictionCatalogue; pub use protocol::metadata::Restriction_Type as RestrictionType; diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs index 9040d71e..af6b58ac 100644 --- a/metadata/src/sale_period.rs +++ b/metadata/src/sale_period.rs @@ -1,11 +1,10 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::{restriction::Restrictions, util::from_repeated_message}; use librespot_core::date::Date; -use librespot_protocol as protocol; +use librespot_protocol as protocol; use protocol::metadata::SalePeriod as SalePeriodMessage; #[derive(Debug, Clone)] diff --git a/metadata/src/show.rs b/metadata/src/show.rs index f69ee021..9f84ba21 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -1,15 +1,16 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; - -use crate::{ - availability::Availabilities, copyright::Copyrights, episode::Episodes, error::RequestError, - image::Images, restriction::Restrictions, Metadata, MetadataError, RequestResult, +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, }; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; -use librespot_protocol as protocol; +use crate::{ + availability::Availabilities, copyright::Copyrights, episode::Episodes, image::Images, + restriction::Restrictions, Metadata, RequestResult, +}; +use librespot_core::{Error, Session, SpotifyId}; + +use librespot_protocol as protocol; pub use protocol::metadata::Show_ConsumptionOrder as ShowConsumptionOrder; pub use protocol::metadata::Show_MediaType as ShowMediaType; @@ -39,20 +40,16 @@ impl Metadata for Show { type Message = protocol::metadata::Show; async fn request(session: &Session, show_id: SpotifyId) -> RequestResult { - session - .spclient() - .get_show_metadata(show_id) - .await - .map_err(RequestError::Http) + session.spclient().get_show_metadata(show_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Self::try_from(msg) } } impl TryFrom<&::Message> for Show { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(show: &::Message) -> Result { Ok(Self { id: show.try_into()?, diff --git a/metadata/src/track.rs b/metadata/src/track.rs index fc9c131e..06efd310 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -1,6 +1,8 @@ -use std::convert::{TryFrom, TryInto}; -use std::fmt::Debug; -use std::ops::Deref; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; use chrono::Local; use uuid::Uuid; @@ -13,17 +15,14 @@ use crate::{ }, availability::{Availabilities, UnavailabilityReason}, content_rating::ContentRatings, - error::RequestError, external_id::ExternalIds, restriction::Restrictions, sale_period::SalePeriods, util::try_from_repeated_message, - Metadata, MetadataError, RequestResult, + Metadata, RequestResult, }; -use librespot_core::date::Date; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; +use librespot_core::{date::Date, Error, Session, SpotifyId}; use librespot_protocol as protocol; #[derive(Debug, Clone)] @@ -105,20 +104,16 @@ impl Metadata for Track { type Message = protocol::metadata::Track; async fn request(session: &Session, track_id: SpotifyId) -> RequestResult { - session - .spclient() - .get_track_metadata(track_id) - .await - .map_err(RequestError::Http) + session.spclient().get_track_metadata(track_id).await } - fn parse(msg: &Self::Message, _: SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: SpotifyId) -> Result { Self::try_from(msg) } } impl TryFrom<&::Message> for Track { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(track: &::Message) -> Result { Ok(Self { id: track.try_into()?, diff --git a/metadata/src/util.rs b/metadata/src/util.rs index d0065221..59142847 100644 --- a/metadata/src/util.rs +++ b/metadata/src/util.rs @@ -27,7 +27,7 @@ pub(crate) use from_repeated_enum; macro_rules! try_from_repeated_message { ($src:ty, $dst:ty) => { impl TryFrom<&[$src]> for $dst { - type Error = MetadataError; + type Error = librespot_core::Error; fn try_from(src: &[$src]) -> Result { let result: Result, _> = src.iter().map(TryFrom::try_from).collect(); Ok(Self(result?)) diff --git a/metadata/src/video.rs b/metadata/src/video.rs index 83f653bb..5e883339 100644 --- a/metadata/src/video.rs +++ b/metadata/src/video.rs @@ -1,11 +1,10 @@ -use std::fmt::Debug; -use std::ops::Deref; +use std::{fmt::Debug, ops::Deref}; use crate::util::from_repeated_message; -use librespot_core::file_id::FileId; -use librespot_protocol as protocol; +use librespot_core::FileId; +use librespot_protocol as protocol; use protocol::metadata::VideoFile as VideoFileMessage; #[derive(Debug, Clone)] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 8946912b..1cd589a5 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -23,9 +23,9 @@ futures-util = { version = "0.3", default_features = false, features = ["alloc"] log = "0.4" byteorder = "1.4" shell-words = "1.0.0" +thiserror = "1.0" tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] } zerocopy = { version = "0.3" } -thiserror = { version = "1" } # Backends alsa = { version = "0.5", optional = true } diff --git a/playback/src/player.rs b/playback/src/player.rs index f0c4acda..c0748987 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,45 +1,40 @@ -use std::cmp::max; -use std::future::Future; -use std::io::{self, Read, Seek, SeekFrom}; -use std::pin::Pin; -use std::process::exit; -use std::task::{Context, Poll}; -use std::time::{Duration, Instant}; -use std::{mem, thread}; +use std::{ + cmp::max, + future::Future, + io::{self, Read, Seek, SeekFrom}, + mem, + pin::Pin, + process::exit, + task::{Context, Poll}, + thread, + time::{Duration, Instant}, +}; use byteorder::{LittleEndian, ReadBytesExt}; -use futures_util::stream::futures_unordered::FuturesUnordered; -use futures_util::{future, StreamExt, TryFutureExt}; -use thiserror::Error; +use futures_util::{future, stream::futures_unordered::FuturesUnordered, StreamExt, TryFutureExt}; use tokio::sync::{mpsc, oneshot}; -use crate::audio::{AudioDecrypt, AudioFile, AudioFileError, StreamLoaderController}; -use crate::audio::{ - READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, - READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, +use crate::{ + audio::{ + AudioDecrypt, AudioFile, StreamLoaderController, READ_AHEAD_BEFORE_PLAYBACK, + READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, + READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, + }, + audio_backend::Sink, + config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, + convert::Converter, + core::{util::SeqGenerator, Error, Session, SpotifyId}, + decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder}, + metadata::audio::{AudioFileFormat, AudioItem}, + mixer::AudioFilter, }; -use crate::audio_backend::Sink; -use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; -use crate::convert::Converter; -use crate::core::session::Session; -use crate::core::spotify_id::SpotifyId; -use crate::core::util::SeqGenerator; -use crate::decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder}; -use crate::metadata::audio::{AudioFileFormat, AudioItem}; -use crate::mixer::AudioFilter; use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND}; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; pub const DB_VOLTAGE_RATIO: f64 = 20.0; -pub type PlayerResult = Result<(), PlayerError>; - -#[derive(Debug, Error)] -pub enum PlayerError { - #[error("audio file error: {0}")] - AudioFile(#[from] AudioFileError), -} +pub type PlayerResult = Result<(), Error>; pub struct Player { commands: Option>, @@ -755,7 +750,7 @@ impl PlayerTrackLoader { let audio = match self.find_available_alternative(audio).await { Some(audio) => audio, None => { - warn!("<{}> is not available", spotify_id.to_uri()); + error!("<{}> is not available", spotify_id.to_uri()); return None; } }; @@ -801,7 +796,7 @@ impl PlayerTrackLoader { let (format, file_id) = match entry { Some(t) => t, None => { - warn!("<{}> is not available in any supported format", audio.name); + error!("<{}> is not available in any supported format", audio.name); return None; } }; @@ -973,7 +968,7 @@ impl Future for PlayerInternal { } } Poll::Ready(Err(e)) => { - warn!( + error!( "Skipping to next track, unable to load track <{:?}>: {:?}", track_id, e ); @@ -1077,7 +1072,7 @@ impl Future for PlayerInternal { } } Err(e) => { - warn!("Skipping to next track, unable to decode samples for track <{:?}>: {:?}", track_id, e); + error!("Skipping to next track, unable to decode samples for track <{:?}>: {:?}", track_id, e); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, @@ -1093,7 +1088,7 @@ impl Future for PlayerInternal { self.handle_packet(packet, normalisation_factor); } Err(e) => { - warn!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e); + error!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, @@ -1128,9 +1123,7 @@ impl Future for PlayerInternal { if (!*suggested_to_preload_next_track) && ((duration_ms as i64 - Self::position_pcm_to_ms(stream_position_pcm) as i64) < PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64) - && stream_loader_controller - .range_to_end_available() - .unwrap_or(false) + && stream_loader_controller.range_to_end_available() { *suggested_to_preload_next_track = true; self.send_event(PlayerEvent::TimeToPreloadNextTrack { @@ -1266,7 +1259,7 @@ impl PlayerInternal { }); self.ensure_sink_running(); } else { - warn!("Player::play called from invalid state"); + error!("Player::play called from invalid state"); } } @@ -1290,7 +1283,7 @@ impl PlayerInternal { duration_ms, }); } else { - warn!("Player::pause called from invalid state"); + error!("Player::pause called from invalid state"); } } @@ -1830,7 +1823,7 @@ impl PlayerInternal { Err(e) => error!("PlayerInternal handle_command_seek: {}", e), } } else { - warn!("Player::seek called from invalid state"); + error!("Player::seek called from invalid state"); } // If we're playing, ensure, that we have enough data leaded to avoid a buffer underrun. @@ -1953,7 +1946,7 @@ impl PlayerInternal { result_rx.map_err(|_| ()) } - fn preload_data_before_playback(&mut self) -> Result<(), PlayerError> { + fn preload_data_before_playback(&mut self) -> PlayerResult { if let PlayerState::Playing { bytes_per_second, ref mut stream_loader_controller, @@ -1978,7 +1971,7 @@ impl PlayerInternal { ); stream_loader_controller .fetch_next_blocking(wait_for_data_length) - .map_err(|e| e.into()) + .map_err(Into::into) } else { Ok(()) } diff --git a/src/main.rs b/src/main.rs index 6bfb027b..0dc25408 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,14 @@ +use std::{ + env, + fs::create_dir_all, + ops::RangeInclusive, + path::{Path, PathBuf}, + pin::Pin, + process::exit, + str::FromStr, + time::{Duration, Instant}, +}; + use futures_util::{future, FutureExt, StreamExt}; use librespot_playback::player::PlayerEvent; use log::{error, info, trace, warn}; @@ -6,35 +17,31 @@ use thiserror::Error; use tokio::sync::mpsc::UnboundedReceiver; use url::Url; -use librespot::connect::spirc::Spirc; -use librespot::core::authentication::Credentials; -use librespot::core::cache::Cache; -use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig}; -use librespot::core::session::Session; -use librespot::core::version; -use librespot::playback::audio_backend::{self, SinkBuilder, BACKENDS}; -use librespot::playback::config::{ - AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, +use librespot::{ + connect::spirc::Spirc, + core::{ + authentication::Credentials, + cache::Cache, + config::{ConnectConfig, DeviceType}, + version, Session, SessionConfig, + }, + playback::{ + audio_backend::{self, SinkBuilder, BACKENDS}, + config::{ + AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, + }, + dither, + mixer::{self, MixerConfig, MixerFn}, + player::{db_to_ratio, ratio_to_db, Player}, + }, }; -use librespot::playback::dither; + #[cfg(feature = "alsa-backend")] use librespot::playback::mixer::alsamixer::AlsaMixer; -use librespot::playback::mixer::{self, MixerConfig, MixerFn}; -use librespot::playback::player::{db_to_ratio, ratio_to_db, Player}; mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; -use std::env; -use std::fs::create_dir_all; -use std::ops::RangeInclusive; -use std::path::{Path, PathBuf}; -use std::pin::Pin; -use std::process::exit; -use std::str::FromStr; -use std::time::Duration; -use std::time::Instant; - fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) } @@ -1530,7 +1537,9 @@ async fn main() { auto_connect_times.clear(); if let Some(spirc) = spirc.take() { - spirc.shutdown(); + if let Err(e) = spirc.shutdown() { + error!("error sending spirc shutdown message: {}", e); + } } if let Some(spirc_task) = spirc_task.take() { // Continue shutdown in its own task @@ -1585,8 +1594,13 @@ async fn main() { } }; - let (spirc_, spirc_task_) = Spirc::new(connect_config, session, player, mixer); - + let (spirc_, spirc_task_) = match Spirc::new(connect_config, session, player, mixer) { + Ok((spirc_, spirc_task_)) => (spirc_, spirc_task_), + Err(e) => { + error!("could not initialize spirc: {}", e); + exit(1); + } + }; spirc = Some(spirc_); spirc_task = Some(Box::pin(spirc_task_)); player_event_channel = Some(event_channel); @@ -1663,7 +1677,9 @@ async fn main() { // Shutdown spirc if necessary if let Some(spirc) = spirc { - spirc.shutdown(); + if let Err(e) = spirc.shutdown() { + error!("error sending spirc shutdown message: {}", e); + } if let Some(mut spirc_task) = spirc_task { tokio::select! { From b4f7a9e35ea987e3b8f1a9bc5ab07a4f864b4e46 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 26 Dec 2021 22:55:45 +0100 Subject: [PATCH 85/95] Change to `parking_lot` and remove remaining panics --- Cargo.lock | 94 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 +- audio/Cargo.toml | 5 +- audio/src/fetch/mod.rs | 40 ++++++++-------- audio/src/fetch/receive.rs | 18 ++++---- connect/Cargo.toml | 2 +- core/Cargo.toml | 11 +++-- core/src/cache.rs | 67 ++++++++++++--------------- core/src/component.rs | 6 +-- core/src/dealer/mod.rs | 20 ++++---- core/src/session.rs | 46 +++++++------------ discovery/Cargo.toml | 6 +-- playback/Cargo.toml | 2 +- 13 files changed, 200 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cce06c16..1d507689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aes" version = "0.6.0" @@ -110,6 +125,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -429,6 +459,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + [[package]] name = "fnv" version = "1.0.7" @@ -569,6 +605,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + [[package]] name = "glib" version = "0.10.3" @@ -1226,6 +1268,7 @@ dependencies = [ "hyper", "librespot-core", "log", + "parking_lot", "tempfile", "thiserror", "tokio", @@ -1278,6 +1321,7 @@ dependencies = [ "num-integer", "num-traits", "once_cell", + "parking_lot", "pbkdf2", "priority-queue", "protobuf", @@ -1432,6 +1476,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.7.14" @@ -1704,6 +1758,15 @@ dependencies = [ "syn", ] +[[package]] +name = "object" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" +dependencies = [ + "memchr", +] + [[package]] name = "oboe" version = "0.4.4" @@ -1771,11 +1834,14 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ + "backtrace", "cfg-if 1.0.0", "instant", "libc", + "petgraph", "redox_syscall", "smallvec", + "thread-id", "winapi", ] @@ -1807,6 +1873,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project" version = "1.0.8" @@ -2115,6 +2191,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2467,6 +2549,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thread-id" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fdfe0627923f7411a43ec9ec9c39c3a9b4151be313e0922042581fb6c9b717f" +dependencies = [ + "libc", + "redox_syscall", + "winapi", +] + [[package]] name = "time" version = "0.1.43" @@ -2505,6 +2598,7 @@ dependencies = [ "mio", "num_cpus", "once_cell", + "parking_lot", "pin-project-lite", "signal-hook-registry", "tokio-macros", diff --git a/Cargo.toml b/Cargo.toml index 8429ba2e..bf453cff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ version = "0.3.1" [dependencies] base64 = "0.13" -env_logger = {version = "0.8", default-features = false, features = ["termcolor","humantime","atty"]} +env_logger = { version = "0.8", default-features = false, features = ["termcolor", "humantime", "atty"] } futures-util = { version = "0.3", default_features = false } getopts = "0.2.21" hex = "0.4" @@ -58,7 +58,7 @@ hyper = "0.14" log = "0.4" rpassword = "5.0" thiserror = "1.0" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "signal", "sync", "process"] } +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" sha-1 = "0.9" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index d5a7a074..c7cf0d7b 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -3,7 +3,7 @@ name = "librespot-audio" version = "0.3.1" authors = ["Paul Lietar "] description="The audio fetching and processing logic for librespot" -license="MIT" +license = "MIT" edition = "2018" [dependencies.librespot-core] @@ -19,6 +19,7 @@ futures-executor = "0.3" futures-util = { version = "0.3", default_features = false } hyper = { version = "0.14", features = ["client"] } log = "0.4" +parking_lot = { version = "0.11", features = ["deadlock_detection"] } tempfile = "3.1" thiserror = "1.0" -tokio = { version = "1", features = ["sync", "macros"] } +tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index dc5bcdf4..3efdc1e9 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -6,13 +6,14 @@ use std::{ io::{self, Read, Seek, SeekFrom}, sync::{ atomic::{self, AtomicUsize}, - Arc, Condvar, Mutex, + Arc, }, time::{Duration, Instant}, }; use futures_util::{future::IntoStream, StreamExt, TryFutureExt}; use hyper::{client::ResponseFuture, header::CONTENT_RANGE, Body, Response, StatusCode}; +use parking_lot::{Condvar, Mutex}; use tempfile::NamedTempFile; use thiserror::Error; use tokio::sync::{mpsc, oneshot}; @@ -159,7 +160,7 @@ impl StreamLoaderController { pub fn range_available(&self, range: Range) -> bool { let available = if let Some(ref shared) = self.stream_shared { - let download_status = shared.download_status.lock().unwrap(); + let download_status = shared.download_status.lock(); range.length <= download_status @@ -214,18 +215,21 @@ impl StreamLoaderController { self.fetch(range); if let Some(ref shared) = self.stream_shared { - let mut download_status = shared.download_status.lock().unwrap(); + let mut download_status = shared.download_status.lock(); while range.length > download_status .downloaded .contained_length_from_value(range.start) { - download_status = shared + if shared .cond - .wait_timeout(download_status, DOWNLOAD_TIMEOUT) - .map_err(|_| AudioFileError::WaitTimeout)? - .0; + .wait_for(&mut download_status, DOWNLOAD_TIMEOUT) + .timed_out() + { + return Err(AudioFileError::WaitTimeout.into()); + } + if range.length > (download_status .downloaded @@ -473,7 +477,7 @@ impl Read for AudioFileStreaming { let length = min(output.len(), self.shared.file_size - offset); - let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) { + let length_to_request = match *(self.shared.download_strategy.lock()) { DownloadStrategy::RandomAccess() => length, DownloadStrategy::Streaming() => { // Due to the read-ahead stuff, we potentially request more than the actual request demanded. @@ -497,7 +501,7 @@ impl Read for AudioFileStreaming { let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length_to_request)); - let mut download_status = self.shared.download_status.lock().unwrap(); + let mut download_status = self.shared.download_status.lock(); ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); @@ -513,17 +517,17 @@ impl Read for AudioFileStreaming { } while !download_status.downloaded.contains(offset) { - download_status = self + if self .shared .cond - .wait_timeout(download_status, DOWNLOAD_TIMEOUT) - .map_err(|_| { - io::Error::new( - io::ErrorKind::TimedOut, - Error::deadline_exceeded(AudioFileError::WaitTimeout), - ) - })? - .0; + .wait_for(&mut download_status, DOWNLOAD_TIMEOUT) + .timed_out() + { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + Error::deadline_exceeded(AudioFileError::WaitTimeout), + )); + } } let available_length = download_status .downloaded diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index f26c95f8..38851129 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -108,7 +108,7 @@ async fn receive_data( if request_length > 0 { let missing_range = Range::new(data_offset, request_length); - let mut download_status = shared.download_status.lock().unwrap(); + let mut download_status = shared.download_status.lock(); download_status.requested.subtract_range(&missing_range); shared.cond.notify_all(); @@ -157,7 +157,7 @@ enum ControlFlow { impl AudioFileFetch { fn get_download_strategy(&mut self) -> DownloadStrategy { - *(self.shared.download_strategy.lock().unwrap()) + *(self.shared.download_strategy.lock()) } fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult { @@ -172,7 +172,7 @@ impl AudioFileFetch { let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length)); - let mut download_status = self.shared.download_status.lock().unwrap(); + let mut download_status = self.shared.download_status.lock(); ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); @@ -218,7 +218,7 @@ impl AudioFileFetch { let mut missing_data = RangeSet::new(); missing_data.add_range(&Range::new(0, self.shared.file_size)); { - let download_status = self.shared.download_status.lock().unwrap(); + let download_status = self.shared.download_status.lock(); missing_data.subtract_range_set(&download_status.downloaded); missing_data.subtract_range_set(&download_status.requested); @@ -306,7 +306,7 @@ impl AudioFileFetch { None => return Err(AudioFileError::Output.into()), } - let mut download_status = self.shared.download_status.lock().unwrap(); + let mut download_status = self.shared.download_status.lock(); let received_range = Range::new(data.offset, data.data.len()); download_status.downloaded.add_range(&received_range); @@ -336,10 +336,10 @@ impl AudioFileFetch { self.download_range(request.start, request.length)?; } StreamLoaderCommand::RandomAccessMode() => { - *(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::RandomAccess(); + *(self.shared.download_strategy.lock()) = DownloadStrategy::RandomAccess(); } StreamLoaderCommand::StreamMode() => { - *(self.shared.download_strategy.lock().unwrap()) = DownloadStrategy::Streaming(); + *(self.shared.download_strategy.lock()) = DownloadStrategy::Streaming(); } StreamLoaderCommand::Close() => return Ok(ControlFlow::Break), } @@ -380,7 +380,7 @@ pub(super) async fn audio_file_fetch( initial_request.offset, initial_request.offset + initial_request.length, ); - let mut download_status = shared.download_status.lock().unwrap(); + let mut download_status = shared.download_status.lock(); download_status.requested.add_range(&requested_range); } @@ -432,7 +432,7 @@ pub(super) async fn audio_file_fetch( let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests; let bytes_pending: usize = { - let download_status = fetch.shared.download_status.lock().unwrap(); + let download_status = fetch.shared.download_status.lock(); download_status .requested diff --git a/connect/Cargo.toml b/connect/Cargo.toml index b0878c1c..ab425a66 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -16,7 +16,7 @@ rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.0", features = ["macros", "sync"] } +tokio = { version = "1.0", features = ["macros", "parking_lot", "sync"] } tokio-stream = "0.1.1" [dependencies.librespot-core] diff --git a/core/Cargo.toml b/core/Cargo.toml index 876a0038..798a5762 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,11 +20,11 @@ bytes = "1" chrono = "0.4" form_urlencoded = "1.0" futures-core = { version = "0.3", default-features = false } -futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "unstable", "sink"] } +futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "sink", "unstable"] } hmac = "0.11" httparse = "1.3" http = "0.2" -hyper = { version = "0.14", features = ["client", "tcp", "http1", "http2"] } +hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] } hyper-proxy = { version = "0.9.1", default-features = false, features = ["rustls"] } hyper-rustls = { version = "0.22", default-features = false, features = ["native-tokio"] } log = "0.4" @@ -34,10 +34,11 @@ num-derive = "0.3" num-integer = "0.1" num-traits = "0.2" once_cell = "1.5.2" +parking_lot = { version = "0.11", features = ["deadlock_detection"] } pbkdf2 = { version = "0.8", default-features = false, features = ["hmac"] } priority-queue = "1.1" protobuf = "2.14.0" -quick-xml = { version = "0.22", features = [ "serialize" ] } +quick-xml = { version = "0.22", features = ["serialize"] } rand = "0.8" rustls = "0.19" rustls-native-certs = "0.5" @@ -46,7 +47,7 @@ serde_json = "1.0" sha-1 = "0.9" shannon = "0.2.0" thiserror = "1.0" -tokio = { version = "1.5", features = ["io-util", "macros", "net", "rt", "time", "sync"] } +tokio = { version = "1.5", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } tokio-stream = "0.1.1" tokio-tungstenite = { version = "0.14", default-features = false, features = ["rustls-tls"] } tokio-util = { version = "0.6", features = ["codec"] } @@ -59,4 +60,4 @@ vergen = "3.0.4" [dev-dependencies] env_logger = "0.8" -tokio = {version = "1.0", features = ["macros"] } +tokio = { version = "1.0", features = ["macros", "parking_lot"] } diff --git a/core/src/cache.rs b/core/src/cache.rs index ed7cf83e..7a3c0fc4 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -4,10 +4,11 @@ use std::{ fs::{self, File}, io::{self, Read, Write}, path::{Path, PathBuf}, - sync::{Arc, Mutex}, + sync::Arc, time::SystemTime, }; +use parking_lot::Mutex; use priority_queue::PriorityQueue; use thiserror::Error; @@ -187,50 +188,42 @@ impl FsSizeLimiter { } } - fn add(&self, file: &Path, size: u64) -> Result<(), Error> { - self.limiter - .lock() - .unwrap() - .add(file, size, SystemTime::now()); - Ok(()) + fn add(&self, file: &Path, size: u64) { + self.limiter.lock().add(file, size, SystemTime::now()); } - fn touch(&self, file: &Path) -> Result { - Ok(self.limiter.lock().unwrap().update(file, SystemTime::now())) + fn touch(&self, file: &Path) -> bool { + self.limiter.lock().update(file, SystemTime::now()) } - fn remove(&self, file: &Path) -> Result { - Ok(self.limiter.lock().unwrap().remove(file)) + fn remove(&self, file: &Path) -> bool { + self.limiter.lock().remove(file) } - fn prune_internal Result, Error>>( - mut pop: F, - ) -> Result<(), Error> { + fn prune_internal Option>(mut pop: F) -> Result<(), Error> { let mut first = true; let mut count = 0; let mut last_error = None; - while let Ok(result) = pop() { - if let Some(file) = result { - if first { - debug!("Cache dir exceeds limit, removing least recently used files."); - first = false; - } - - let res = fs::remove_file(&file); - if let Err(e) = res { - warn!("Could not remove file {:?} from cache dir: {}", file, e); - last_error = Some(e); - } else { - count += 1; - } + while let Some(file) = pop() { + if first { + debug!("Cache dir exceeds limit, removing least recently used files."); + first = false; } - if count > 0 { - info!("Removed {} cache files.", count); + let res = fs::remove_file(&file); + if let Err(e) = res { + warn!("Could not remove file {:?} from cache dir: {}", file, e); + last_error = Some(e); + } else { + count += 1; } } + if count > 0 { + info!("Removed {} cache files.", count); + } + if let Some(err) = last_error { Err(err.into()) } else { @@ -239,14 +232,14 @@ impl FsSizeLimiter { } fn prune(&self) -> Result<(), Error> { - Self::prune_internal(|| Ok(self.limiter.lock().unwrap().pop())) + Self::prune_internal(|| self.limiter.lock().pop()) } fn new(path: &Path, limit: u64) -> Result { let mut limiter = SizeLimiter::new(limit); Self::init_dir(&mut limiter, path); - Self::prune_internal(|| Ok(limiter.pop()))?; + Self::prune_internal(|| limiter.pop())?; Ok(Self { limiter: Mutex::new(limiter), @@ -388,8 +381,8 @@ impl Cache { match File::open(&path) { Ok(file) => { if let Some(limiter) = self.size_limiter.as_deref() { - if let Err(e) = limiter.touch(&path) { - error!("limiter could not touch {:?}: {}", path, e); + if !limiter.touch(&path) { + error!("limiter could not touch {:?}", path); } } Some(file) @@ -411,8 +404,8 @@ impl Cache { .and_then(|mut file| io::copy(contents, &mut file)) { if let Some(limiter) = self.size_limiter.as_deref() { - limiter.add(&path, size)?; - limiter.prune()? + limiter.add(&path, size); + limiter.prune()?; } return Ok(()); } @@ -426,7 +419,7 @@ impl Cache { fs::remove_file(&path)?; if let Some(limiter) = self.size_limiter.as_deref() { - limiter.remove(&path)?; + limiter.remove(&path); } Ok(()) diff --git a/core/src/component.rs b/core/src/component.rs index aa1da840..ebe42e8d 100644 --- a/core/src/component.rs +++ b/core/src/component.rs @@ -1,20 +1,20 @@ macro_rules! component { ($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => { #[derive(Clone)] - pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::std::sync::Mutex<$inner>)>); + pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::parking_lot::Mutex<$inner>)>); impl $name { #[allow(dead_code)] pub(crate) fn new(session: $crate::session::SessionWeak) -> $name { debug!(target:"librespot::component", "new {}", stringify!($name)); - $name(::std::sync::Arc::new((session, ::std::sync::Mutex::new($inner { + $name(::std::sync::Arc::new((session, ::parking_lot::Mutex::new($inner { $($key : $value,)* })))) } #[allow(dead_code)] fn lock R, R>(&self, f: F) -> R { - let mut inner = (self.0).1.lock().unwrap(); + let mut inner = (self.0).1.lock(); f(&mut inner) } diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index ac19fd6d..c1a9c94d 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -6,7 +6,7 @@ use std::{ pin::Pin, sync::{ atomic::{self, AtomicBool}, - Arc, Mutex, + Arc, }, task::Poll, time::Duration, @@ -14,6 +14,7 @@ use std::{ use futures_core::{Future, Stream}; use futures_util::{future::join_all, SinkExt, StreamExt}; +use parking_lot::Mutex; use thiserror::Error; use tokio::{ select, @@ -310,7 +311,6 @@ impl DealerShared { if let Some(split) = split_uri(&msg.uri) { self.message_handlers .lock() - .unwrap() .retain(split, &mut |tx| tx.send(msg.clone()).is_ok()); } } @@ -330,7 +330,7 @@ impl DealerShared { }; { - let handler_map = self.request_handlers.lock().unwrap(); + let handler_map = self.request_handlers.lock(); if let Some(handler) = handler_map.get(split) { handler.handle_request(request, responder); @@ -349,7 +349,9 @@ impl DealerShared { } async fn closed(&self) { - self.notify_drop.acquire().await.unwrap_err(); + if self.notify_drop.acquire().await.is_ok() { + error!("should never have gotten a permit"); + } } fn is_closed(&self) -> bool { @@ -367,19 +369,15 @@ impl Dealer { where H: RequestHandler, { - add_handler( - &mut self.shared.request_handlers.lock().unwrap(), - uri, - handler, - ) + add_handler(&mut self.shared.request_handlers.lock(), uri, handler) } pub fn remove_handler(&self, uri: &str) -> Option> { - remove_handler(&mut self.shared.request_handlers.lock().unwrap(), uri) + remove_handler(&mut self.shared.request_handlers.lock(), uri) } pub fn subscribe(&self, uris: &[&str]) -> Result { - subscribe(&mut self.shared.message_handlers.lock().unwrap(), uris) + subscribe(&mut self.shared.message_handlers.lock(), uris) } pub async fn close(mut self) { diff --git a/core/src/session.rs b/core/src/session.rs index 72805551..f1136e53 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -6,7 +6,7 @@ use std::{ process::exit, sync::{ atomic::{AtomicUsize, Ordering}, - Arc, RwLock, Weak, + Arc, Weak, }, task::{Context, Poll}, time::{SystemTime, UNIX_EPOCH}, @@ -18,6 +18,7 @@ use futures_core::TryStream; use futures_util::{future, ready, StreamExt, TryStreamExt}; use num_traits::FromPrimitive; use once_cell::sync::OnceCell; +use parking_lot::RwLock; use quick_xml::events::Event; use thiserror::Error; use tokio::sync::mpsc; @@ -138,8 +139,7 @@ impl Session { connection::authenticate(&mut transport, credentials, &session.config().device_id) .await?; info!("Authenticated as \"{}\" !", reusable_credentials.username); - session.0.data.write().unwrap().user_data.canonical_username = - reusable_credentials.username.clone(); + session.0.data.write().user_data.canonical_username = reusable_credentials.username.clone(); if let Some(cache) = session.cache() { cache.save_credentials(&reusable_credentials); } @@ -200,7 +200,7 @@ impl Session { } pub fn time_delta(&self) -> i64 { - self.0.data.read().unwrap().time_delta + self.0.data.read().time_delta } pub fn spawn(&self, task: T) @@ -253,7 +253,7 @@ impl Session { } .as_secs() as i64; - self.0.data.write().unwrap().time_delta = server_timestamp - timestamp; + self.0.data.write().time_delta = server_timestamp - timestamp; self.debug_info(); self.send_packet(Pong, vec![0, 0, 0, 0]) @@ -261,7 +261,7 @@ impl Session { Some(CountryCode) => { let country = String::from_utf8(data.as_ref().to_owned())?; info!("Country: {:?}", country); - self.0.data.write().unwrap().user_data.country = country; + self.0.data.write().user_data.country = country; Ok(()) } Some(StreamChunkRes) | Some(ChannelError) => self.channel().dispatch(cmd, data), @@ -306,7 +306,7 @@ impl Session { trace!("Received product info: {:#?}", user_attributes); Self::check_catalogue(&user_attributes); - self.0.data.write().unwrap().user_data.attributes = user_attributes; + self.0.data.write().user_data.attributes = user_attributes; Ok(()) } Some(PongAck) @@ -335,7 +335,7 @@ impl Session { } pub fn user_data(&self) -> UserData { - self.0.data.read().unwrap().user_data.clone() + self.0.data.read().user_data.clone() } pub fn device_id(&self) -> &str { @@ -343,21 +343,15 @@ impl Session { } pub fn connection_id(&self) -> String { - self.0.data.read().unwrap().connection_id.clone() + self.0.data.read().connection_id.clone() } pub fn set_connection_id(&self, connection_id: String) { - self.0.data.write().unwrap().connection_id = connection_id; + self.0.data.write().connection_id = connection_id; } pub fn username(&self) -> String { - self.0 - .data - .read() - .unwrap() - .user_data - .canonical_username - .clone() + self.0.data.read().user_data.canonical_username.clone() } pub fn set_user_attribute(&self, key: &str, value: &str) -> Option { @@ -368,7 +362,6 @@ impl Session { self.0 .data .write() - .unwrap() .user_data .attributes .insert(key.to_owned(), value.to_owned()) @@ -377,13 +370,7 @@ impl Session { pub fn set_user_attributes(&self, attributes: UserAttributes) { Self::check_catalogue(&attributes); - self.0 - .data - .write() - .unwrap() - .user_data - .attributes - .extend(attributes) + self.0.data.write().user_data.attributes.extend(attributes) } fn weak(&self) -> SessionWeak { @@ -395,14 +382,14 @@ impl Session { } pub fn shutdown(&self) { - debug!("Invalidating session[{}]", self.0.session_id); - self.0.data.write().unwrap().invalid = true; + debug!("Invalidating session [{}]", self.0.session_id); + self.0.data.write().invalid = true; self.mercury().shutdown(); self.channel().shutdown(); } pub fn is_invalid(&self) -> bool { - self.0.data.read().unwrap().invalid + self.0.data.read().invalid } } @@ -415,7 +402,8 @@ impl SessionWeak { } pub(crate) fn upgrade(&self) -> Session { - self.try_upgrade().expect("Session died") // TODO + self.try_upgrade() + .expect("session was dropped and so should have this component") } } diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 7edd934a..a5c56bbb 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -15,14 +15,14 @@ form_urlencoded = "1.0" futures-core = "0.3" futures-util = "0.3" hmac = "0.11" -hyper = { version = "0.14", features = ["server", "http1", "tcp"] } +hyper = { version = "0.14", features = ["http1", "server", "tcp"] } libmdns = "0.6" log = "0.4" rand = "0.8" serde_json = "1.0.25" sha-1 = "0.9" thiserror = "1.0" -tokio = { version = "1.0", features = ["sync", "rt"] } +tokio = { version = "1.0", features = ["parking_lot", "sync", "rt"] } dns-sd = { version = "0.1.3", optional = true } @@ -34,7 +34,7 @@ version = "0.3.1" [dev-dependencies] futures = "0.3" hex = "0.4" -tokio = { version = "1.0", features = ["macros", "rt"] } +tokio = { version = "1.0", features = ["macros", "parking_lot", "rt"] } [features] with-dns-sd = ["dns-sd"] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 1cd589a5..fee4dd51 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -24,7 +24,7 @@ log = "0.4" byteorder = "1.4" shell-words = "1.0.0" thiserror = "1.0" -tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] } +tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } zerocopy = { version = "0.3" } # Backends From 059e17dca591949d3609dfb0372a78b31770c005 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 26 Dec 2021 23:51:25 +0100 Subject: [PATCH 86/95] Fix tests --- core/src/spotify_id.rs | 22 ++++------------------ core/tests/connect.rs | 2 +- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 15b365b0..b8a1448e 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -516,7 +516,6 @@ mod tests { struct ConversionCase { id: u128, kind: SpotifyItemType, - uri_error: Option, uri: &'static str, base16: &'static str, base62: &'static str, @@ -527,7 +526,6 @@ mod tests { ConversionCase { id: 238762092608182713602505436543891614649, kind: SpotifyItemType::Track, - uri_error: None, uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH", base16: "b39fe8081e1f4c54be38e8d6f9f12bb9", base62: "5sWHDYs0csV6RS48xBl0tH", @@ -538,7 +536,6 @@ mod tests { ConversionCase { id: 204841891221366092811751085145916697048, kind: SpotifyItemType::Track, - uri_error: None, uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", @@ -549,7 +546,6 @@ mod tests { ConversionCase { id: 204841891221366092811751085145916697048, kind: SpotifyItemType::Episode, - uri_error: None, uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", @@ -560,7 +556,6 @@ mod tests { ConversionCase { id: 204841891221366092811751085145916697048, kind: SpotifyItemType::Show, - uri_error: None, uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", @@ -575,7 +570,6 @@ mod tests { id: 0, kind: SpotifyItemType::Unknown, // Invalid ID in the URI. - uri_error: SpotifyIdError::InvalidId, uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH", base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9", base62: "!!!!!Ys0csV6RS48xBl0tH", @@ -588,7 +582,6 @@ mod tests { id: 0, kind: SpotifyItemType::Unknown, // Missing colon between ID and type. - uri_error: SpotifyIdError::InvalidFormat, uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", base16: "--------------------", base62: "....................", @@ -601,7 +594,6 @@ mod tests { id: 0, kind: SpotifyItemType::Unknown, // Uri too short - uri_error: SpotifyIdError::InvalidId, uri: "spotify:azb:aRS48xBl0tH", base16: "--------------------", base62: "....................", @@ -619,10 +611,7 @@ mod tests { } for c in &CONV_INVALID { - assert_eq!( - SpotifyId::from_base62(c.base62), - Err(SpotifyIdError::InvalidId) - ); + assert!(SpotifyId::from_base62(c.base62).is_err(),); } } @@ -645,10 +634,7 @@ mod tests { } for c in &CONV_INVALID { - assert_eq!( - SpotifyId::from_base16(c.base16), - Err(SpotifyIdError::InvalidId) - ); + assert!(SpotifyId::from_base16(c.base16).is_err(),); } } @@ -674,7 +660,7 @@ mod tests { } for c in &CONV_INVALID { - assert_eq!(SpotifyId::from_uri(c.uri), Err(c.uri_error.unwrap())); + assert!(SpotifyId::from_uri(c.uri).is_err()); } } @@ -697,7 +683,7 @@ mod tests { } for c in &CONV_INVALID { - assert_eq!(SpotifyId::from_raw(c.raw), Err(SpotifyIdError::InvalidId)); + assert!(SpotifyId::from_raw(c.raw).is_err()); } } } diff --git a/core/tests/connect.rs b/core/tests/connect.rs index 8b95e437..19d7977e 100644 --- a/core/tests/connect.rs +++ b/core/tests/connect.rs @@ -18,7 +18,7 @@ async fn test_connection() { match result { Ok(_) => panic!("Authentication succeeded despite of bad credentials."), - Err(e) => assert_eq!(e.to_string(), "Login failed with reason: Bad credentials"), + Err(e) => assert!(!e.to_string().is_empty()), // there should be some error message } }) .await From 8aa23ed0c6e102cb4992565e10cb43e67bf8c349 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 27 Dec 2021 00:11:07 +0100 Subject: [PATCH 87/95] Drop locks as soon as possible --- audio/src/fetch/receive.rs | 37 ++++++++++++++++++++----------------- core/src/cache.rs | 2 +- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 38851129..41f4ef84 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -106,12 +106,12 @@ async fn receive_data( drop(request.streamer); if request_length > 0 { - let missing_range = Range::new(data_offset, request_length); - - let mut download_status = shared.download_status.lock(); - - download_status.requested.subtract_range(&missing_range); - shared.cond.notify_all(); + { + let missing_range = Range::new(data_offset, request_length); + let mut download_status = shared.download_status.lock(); + download_status.requested.subtract_range(&missing_range); + shared.cond.notify_all(); + } } shared @@ -172,14 +172,18 @@ impl AudioFileFetch { let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length)); + // The iteration that follows spawns streamers fast, without awaiting them, + // so holding the lock for the entire scope of this function should be faster + // then locking and unlocking multiple times. let mut download_status = self.shared.download_status.lock(); ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); - for range in ranges_to_request.iter() { - let url = self.shared.cdn_url.try_get_url()?; + // Likewise, checking for the URL expiry once will guarantee validity long enough. + let url = self.shared.cdn_url.try_get_url()?; + for range in ranges_to_request.iter() { let streamer = self .session .spclient() @@ -219,7 +223,6 @@ impl AudioFileFetch { missing_data.add_range(&Range::new(0, self.shared.file_size)); { let download_status = self.shared.download_status.lock(); - missing_data.subtract_range_set(&download_status.downloaded); missing_data.subtract_range_set(&download_status.requested); } @@ -306,16 +309,16 @@ impl AudioFileFetch { None => return Err(AudioFileError::Output.into()), } - let mut download_status = self.shared.download_status.lock(); - let received_range = Range::new(data.offset, data.data.len()); - download_status.downloaded.add_range(&received_range); - self.shared.cond.notify_all(); - let full = download_status.downloaded.contained_length_from_value(0) - >= self.shared.file_size; + let full = { + let mut download_status = self.shared.download_status.lock(); + download_status.downloaded.add_range(&received_range); + self.shared.cond.notify_all(); - drop(download_status); + download_status.downloaded.contained_length_from_value(0) + >= self.shared.file_size + }; if full { self.finish()?; @@ -380,8 +383,8 @@ pub(super) async fn audio_file_fetch( initial_request.offset, initial_request.offset + initial_request.length, ); - let mut download_status = shared.download_status.lock(); + let mut download_status = shared.download_status.lock(); download_status.requested.add_range(&requested_range); } diff --git a/core/src/cache.rs b/core/src/cache.rs index 7a3c0fc4..9484bb16 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -189,7 +189,7 @@ impl FsSizeLimiter { } fn add(&self, file: &Path, size: u64) { - self.limiter.lock().add(file, size, SystemTime::now()); + self.limiter.lock().add(file, size, SystemTime::now()) } fn touch(&self, file: &Path) -> bool { From 95776de74a5297e03fc74a8d81bb835c29dbd4c2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 27 Dec 2021 00:21:42 +0100 Subject: [PATCH 88/95] Fix compilation for with-dns-sd --- Cargo.lock | 1 + Cargo.toml | 2 +- core/Cargo.toml | 4 ++++ core/src/error.rs | 10 ++++++++++ discovery/Cargo.toml | 3 +-- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d507689..81f083ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1303,6 +1303,7 @@ dependencies = [ "byteorder", "bytes", "chrono", + "dns-sd", "env_logger", "form_urlencoded", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index bf453cff..5a501ef5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ rodiojack-backend = ["librespot-playback/rodiojack-backend"] sdl-backend = ["librespot-playback/sdl-backend"] gstreamer-backend = ["librespot-playback/gstreamer-backend"] -with-dns-sd = ["librespot-discovery/with-dns-sd"] +with-dns-sd = ["librespot-core/with-dns-sd", "librespot-discovery/with-dns-sd"] default = ["rodio-backend"] diff --git a/core/Cargo.toml b/core/Cargo.toml index 798a5762..271e5896 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -18,6 +18,7 @@ base64 = "0.13" byteorder = "1.4" bytes = "1" chrono = "0.4" +dns-sd = { version = "0.1.3", optional = true } form_urlencoded = "1.0" futures-core = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "sink", "unstable"] } @@ -61,3 +62,6 @@ vergen = "3.0.4" [dev-dependencies] env_logger = "0.8" tokio = { version = "1.0", features = ["macros", "parking_lot"] } + +[features] +with-dns-sd = ["dns-sd"] diff --git a/core/src/error.rs b/core/src/error.rs index e3753014..d032bd2a 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -12,6 +12,9 @@ use thiserror::Error; use tokio::sync::{mpsc::error::SendError, oneshot::error::RecvError}; use url::ParseError; +#[cfg(feature = "with-dns-sd")] +use dns_sd::DNSError; + #[derive(Debug)] pub struct Error { pub kind: ErrorKind, @@ -283,6 +286,13 @@ impl From for Error { } } +#[cfg(feature = "with-dns-sd")] +impl From for Error { + fn from(err: DNSError) -> Self { + Self::new(ErrorKind::Unavailable, err) + } +} + impl From for Error { fn from(err: http::Error) -> Self { if err.is::() diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index a5c56bbb..17edf286 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -11,6 +11,7 @@ edition = "2018" aes-ctr = "0.6" base64 = "0.13" cfg-if = "1.0" +dns-sd = { version = "0.1.3", optional = true } form_urlencoded = "1.0" futures-core = "0.3" futures-util = "0.3" @@ -24,8 +25,6 @@ sha-1 = "0.9" thiserror = "1.0" tokio = { version = "1.0", features = ["parking_lot", "sync", "rt"] } -dns-sd = { version = "0.1.3", optional = true } - [dependencies.librespot-core] path = "../core" default_features = false From b622e3811e468ca217e9ea41389f41183be7abab Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 27 Dec 2021 00:45:27 +0100 Subject: [PATCH 89/95] Enable HTTP/2 flow control --- core/src/http_client.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 2dc21355..1cdfcf75 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -156,7 +156,8 @@ impl HttpClient { pub fn request_fut(&self, mut req: Request) -> Result { let mut http = HttpConnector::new(); http.enforce_http(false); - let connector = HttpsConnector::from((http, self.tls_config.clone())); + + let https_connector = HttpsConnector::from((http, self.tls_config.clone())); let headers_mut = req.headers_mut(); headers_mut.insert(USER_AGENT, self.user_agent.clone()); @@ -164,11 +165,14 @@ impl HttpClient { let request = if let Some(url) = &self.proxy { let proxy_uri = url.to_string().parse()?; let proxy = Proxy::new(Intercept::All, proxy_uri); - let proxy_connector = ProxyConnector::from_proxy(connector, proxy)?; + let proxy_connector = ProxyConnector::from_proxy(https_connector, proxy)?; Client::builder().build(proxy_connector).request(req) } else { - Client::builder().build(connector).request(req) + Client::builder() + .http2_adaptive_window(true) + .build(https_connector) + .request(req) }; Ok(request) From 643b39b40ea8c302f5df9bcb33f337b480634190 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 27 Dec 2021 00:47:17 +0100 Subject: [PATCH 90/95] Fix discovery compilation with-dns-sd --- discovery/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 17edf286..0225ab68 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -27,7 +27,6 @@ tokio = { version = "1.0", features = ["parking_lot", "sync", "rt"] } [dependencies.librespot-core] path = "../core" -default_features = false version = "0.3.1" [dev-dependencies] @@ -36,4 +35,4 @@ hex = "0.4" tokio = { version = "1.0", features = ["macros", "parking_lot", "rt"] } [features] -with-dns-sd = ["dns-sd"] +with-dns-sd = ["dns-sd", "librespot-core/with-dns-sd"] From b7c047bca2404252b9fa5b631e31a7719efe83fb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 27 Dec 2021 09:35:11 +0100 Subject: [PATCH 91/95] Fix alternative tracks --- playback/src/player.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index c0748987..2c5d25c3 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -694,7 +694,10 @@ struct PlayerTrackLoader { impl PlayerTrackLoader { async fn find_available_alternative(&self, audio: AudioItem) -> Option { - if audio.availability.is_ok() { + if let Err(e) = audio.availability { + error!("Track is unavailable: {}", e); + None + } else if !audio.files.is_empty() { Some(audio) } else if let Some(alternatives) = &audio.alternatives { let alternatives: FuturesUnordered<_> = alternatives @@ -708,6 +711,7 @@ impl PlayerTrackLoader { .next() .await } else { + error!("Track should be available, but no alternatives found."); None } } From 01fb6044205a064c022bebbe703825fb55a59093 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 27 Dec 2021 09:47:51 +0100 Subject: [PATCH 92/95] Allow failures on nightly Rust --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e447ff9..e7c5514b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,7 +67,6 @@ jobs: - 1.48 # MSRV (Minimum supported rust version) - stable - beta - experimental: [false] # Ignore failures in nightly include: - os: ubuntu-latest From 4646ff3075f1af48fe0d3fe0f938b2bd7a4325b9 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 27 Dec 2021 11:35:05 +0100 Subject: [PATCH 93/95] Re-order actions and fail on clippy lints --- .github/workflows/test.yml | 135 ++++++++++++++++++++++++++----------- rustfmt.toml | 3 - 2 files changed, 96 insertions(+), 42 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7c5514b..30848c9b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,32 +31,20 @@ on: "!LICENSE", "!*.sh", ] - schedule: - # Run CI every week - - cron: "00 01 * * 0" env: RUST_BACKTRACE: 1 + RUSTFLAGS: -D warnings + +# The layering here is as follows, checking in priority from highest to lowest: +# 1. absence of errors and warnings on Linux/x86 +# 2. cross compilation on Windows and Linux/ARM +# 3. absence of lints +# 4. code formatting jobs: - fmt: - name: rustfmt - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Install toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - components: rustfmt - - run: cargo fmt --all -- --check - test-linux: - needs: fmt - name: cargo +${{ matrix.toolchain }} build (${{ matrix.os }}) + name: cargo +${{ matrix.toolchain }} check (${{ matrix.os }}) runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} strategy: @@ -66,11 +54,11 @@ jobs: toolchain: - 1.48 # MSRV (Minimum supported rust version) - stable - - beta - # Ignore failures in nightly + experimental: [false] + # Ignore failures in beta include: - os: ubuntu-latest - toolchain: nightly + toolchain: beta experimental: true steps: - name: Checkout code @@ -105,22 +93,25 @@ jobs: - run: cargo test --workspace - run: cargo install cargo-hack - - run: cargo hack --workspace --remove-dev-deps - - run: cargo build -p librespot-core --no-default-features - - run: cargo build -p librespot-core - - run: cargo hack build --each-feature -p librespot-discovery - - run: cargo hack build --each-feature -p librespot-playback - - run: cargo hack build --each-feature + - run: cargo hack --workspace --remove-dev-deps + - run: cargo check -p librespot-core --no-default-features + - run: cargo check -p librespot-core + - run: cargo hack check --each-feature -p librespot-discovery + - run: cargo hack check --each-feature -p librespot-playback + - run: cargo hack check --each-feature test-windows: - needs: fmt - name: cargo build (${{ matrix.os }}) + needs: test-linux + name: cargo +${{ matrix.toolchain }} check (${{ matrix.os }}) runs-on: ${{ matrix.os }} + continue-on-error: false strategy: fail-fast: false matrix: os: [windows-latest] - toolchain: [stable] + toolchain: + - 1.48 # MSRV (Minimum supported rust version) + - stable steps: - name: Checkout code uses: actions/checkout@v2 @@ -152,20 +143,22 @@ jobs: - run: cargo install cargo-hack - run: cargo hack --workspace --remove-dev-deps - - run: cargo build --no-default-features - - run: cargo build + - run: cargo check --no-default-features + - run: cargo check test-cross-arm: - needs: fmt + name: cross +${{ matrix.toolchain }} build ${{ matrix.target }} + needs: test-linux runs-on: ${{ matrix.os }} continue-on-error: false strategy: fail-fast: false matrix: - include: - - os: ubuntu-latest - target: armv7-unknown-linux-gnueabihf - toolchain: stable + os: [ubuntu-latest] + target: [armv7-unknown-linux-gnueabihf] + toolchain: + - 1.48 # MSRV (Minimum supported rust version) + - stable steps: - name: Checkout code uses: actions/checkout@v2 @@ -196,3 +189,67 @@ jobs: run: cargo install cross || true - name: Build run: cross build --locked --target ${{ matrix.target }} --no-default-features + + clippy: + needs: [test-cross-arm, test-windows] + name: cargo +${{ matrix.toolchain }} clippy (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + continue-on-error: false + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + toolchain: [stable] + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.toolchain }} + override: true + components: clippy + + - name: Get Rustc version + id: get-rustc-version + run: echo "::set-output name=version::$(rustc -V)" + shell: bash + + - name: Cache Rust dependencies + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + target + key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock') }} + + - name: Install developer package dependencies + run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev libavahi-compat-libdnssd-dev + + - run: cargo install cargo-hack + - run: cargo hack --workspace --remove-dev-deps + - run: cargo clippy -p librespot-core --no-default-features + - run: cargo clippy -p librespot-core + - run: cargo hack clippy --each-feature -p librespot-discovery + - run: cargo hack clippy --each-feature -p librespot-playback + - run: cargo hack clippy --each-feature + + fmt: + needs: clippy + name: cargo +${{ matrix.toolchain }} fmt + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt + - run: cargo fmt --all -- --check diff --git a/rustfmt.toml b/rustfmt.toml index aefd6aa8..32a9786f 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1 @@ -# max_width = 105 -reorder_imports = true -reorder_modules = true edition = "2018" From 0f78fc277e1ef580cc4f51ade726cb31bb1878e0 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 27 Dec 2021 21:37:22 +0100 Subject: [PATCH 94/95] Call `stream_from_cdn` with `CdnUrl` --- audio/src/fetch/mod.rs | 9 +++++---- audio/src/fetch/receive.rs | 14 ++++++++------ core/src/cdn_url.rs | 10 ++++++++-- core/src/spclient.rs | 8 +++++--- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 3efdc1e9..346a786f 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -399,12 +399,13 @@ impl AudioFileStreaming { INITIAL_DOWNLOAD_SIZE }; + trace!("Streaming {}", file_id); + let cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?; - let url = cdn_url.try_get_url()?; - trace!("Streaming {:?}", url); - - let mut streamer = session.spclient().stream_file(url, 0, download_size)?; + let mut streamer = session + .spclient() + .stream_from_cdn(&cdn_url, 0, download_size)?; let request_time = Instant::now(); // Get the first chunk with the headers to get the file size. diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 41f4ef84..e04c58d2 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -43,6 +43,8 @@ async fn receive_data( let mut data_offset = requested_offset; let mut request_length = requested_length; + // TODO : check Content-Length and Content-Range headers + let old_number_of_request = shared .number_of_open_requests .fetch_add(1, Ordering::SeqCst); @@ -180,14 +182,14 @@ impl AudioFileFetch { ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); - // Likewise, checking for the URL expiry once will guarantee validity long enough. - let url = self.shared.cdn_url.try_get_url()?; + // TODO : refresh cdn_url when the token expired for range in ranges_to_request.iter() { - let streamer = self - .session - .spclient() - .stream_file(url, range.start, range.length)?; + let streamer = self.session.spclient().stream_from_cdn( + &self.shared.cdn_url, + range.start, + range.length, + )?; download_status.requested.add_range(range); diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 409d7f25..befdefd6 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -39,13 +39,15 @@ pub enum CdnUrlError { Expired, #[error("resolved storage is not for CDN")] Storage, + #[error("no URLs resolved")] + Unresolved, } impl From for Error { fn from(err: CdnUrlError) -> Self { match err { CdnUrlError::Expired => Error::deadline_exceeded(err), - CdnUrlError::Storage => Error::unavailable(err), + CdnUrlError::Storage | CdnUrlError::Unresolved => Error::unavailable(err), } } } @@ -66,7 +68,7 @@ impl CdnUrl { pub async fn resolve_audio(&self, session: &Session) -> Result { let file_id = self.file_id; - let response = session.spclient().get_audio_urls(file_id).await?; + let response = session.spclient().get_audio_storage(file_id).await?; let msg = CdnUrlMessage::parse_from_bytes(&response)?; let urls = MaybeExpiringUrls::try_from(msg)?; @@ -78,6 +80,10 @@ impl CdnUrl { } pub fn try_get_url(&self) -> Result<&str, Error> { + if self.urls.is_empty() { + return Err(CdnUrlError::Unresolved.into()); + } + let now = Local::now(); let url = self.urls.iter().find(|url| match url.1 { Some(expiry) => now < expiry.as_utc(), diff --git a/core/src/spclient.rs b/core/src/spclient.rs index c4285cd4..1adfa3f8 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -13,6 +13,7 @@ use rand::Rng; use crate::{ apresolve::SocketAddress, + cdn_url::CdnUrl, error::ErrorKind, protocol::{ canvaz::EntityCanvazRequest, connect::PutStateRequest, @@ -261,7 +262,7 @@ impl SpClient { .await } - pub async fn get_audio_urls(&self, file_id: FileId) -> SpClientResult { + pub async fn get_audio_storage(&self, file_id: FileId) -> SpClientResult { let endpoint = format!( "/storage-resolve/files/audio/interactive/{}", file_id.to_base16() @@ -269,12 +270,13 @@ impl SpClient { self.request(&Method::GET, &endpoint, None, None).await } - pub fn stream_file( + pub fn stream_from_cdn( &self, - url: &str, + cdn_url: &CdnUrl, offset: usize, length: usize, ) -> Result, Error> { + let url = cdn_url.try_get_url()?; let req = Request::builder() .method(&Method::GET) .uri(url) From 332f9f04b11ff058893c000b13c5b2162e738246 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 28 Dec 2021 23:46:37 +0100 Subject: [PATCH 95/95] Fix error hitting play when loading Further changes: - Improve some debug and trace messages - Default to streaming download strategy - Synchronize mixer volume on loading play - Use default normalisation values when the file position isn't exactly what we need it to be - Update track position only when the decoder reports a successful seek --- audio/src/fetch/mod.rs | 12 ++-- connect/src/spirc.rs | 44 ++++++++------- playback/src/player.rs | 125 ++++++++++++++++++++++++++++------------- 3 files changed, 116 insertions(+), 65 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 346a786f..f9e85d10 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -341,6 +341,8 @@ impl AudioFile { let session_ = session.clone(); session.spawn(complete_rx.map_ok(move |mut file| { + debug!("Downloading file {} complete", file_id); + if let Some(cache) = session_.cache() { if let Some(cache_id) = cache.file(file_id) { if let Err(e) = cache.save_file(file_id, &mut file) { @@ -349,8 +351,6 @@ impl AudioFile { debug!("File {} cached to {:?}", file_id, cache_id); } } - - debug!("Downloading file {} complete", file_id); } })); @@ -399,10 +399,12 @@ impl AudioFileStreaming { INITIAL_DOWNLOAD_SIZE }; - trace!("Streaming {}", file_id); - let cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?; + if let Ok(url) = cdn_url.try_get_url() { + trace!("Streaming from {}", url); + } + let mut streamer = session .spclient() .stream_from_cdn(&cdn_url, 0, download_size)?; @@ -438,7 +440,7 @@ impl AudioFileStreaming { requested: RangeSet::new(), downloaded: RangeSet::new(), }), - download_strategy: Mutex::new(DownloadStrategy::RandomAccess()), // start with random access mode until someone tells us otherwise + download_strategy: Mutex::new(DownloadStrategy::Streaming()), number_of_open_requests: AtomicUsize::new(0), ping_time_ms: AtomicUsize::new(0), read_position: AtomicUsize::new(0), diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index dc631831..144b9f24 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -626,7 +626,11 @@ impl SpircTask { if Some(play_request_id) == self.play_request_id { match event { PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(), - PlayerEvent::Loading { .. } => self.notify(None, false), + PlayerEvent::Loading { .. } => { + trace!("==> kPlayStatusLoading"); + self.state.set_status(PlayStatus::kPlayStatusLoading); + self.notify(None, false) + } PlayerEvent::Playing { position_ms, .. } => { trace!("==> kPlayStatusPlay"); let new_nominal_start_time = self.now_ms() - position_ms as i64; @@ -687,15 +691,18 @@ impl SpircTask { _ => Ok(()), } } - PlayerEvent::Stopped { .. } => match self.play_status { - SpircPlayStatus::Stopped => Ok(()), - _ => { - warn!("The player has stopped unexpectedly."); - self.state.set_status(PlayStatus::kPlayStatusStop); - self.play_status = SpircPlayStatus::Stopped; - self.notify(None, true) + PlayerEvent::Stopped { .. } => { + trace!("==> kPlayStatusStop"); + match self.play_status { + SpircPlayStatus::Stopped => Ok(()), + _ => { + warn!("The player has stopped unexpectedly."); + self.state.set_status(PlayStatus::kPlayStatusStop); + self.play_status = SpircPlayStatus::Stopped; + self.notify(None, true) + } } - }, + } PlayerEvent::TimeToPreloadNextTrack { .. } => { self.handle_preload_next_track(); Ok(()) @@ -923,12 +930,6 @@ impl SpircTask { position_ms, preloading_of_next_track_triggered, } => { - // TODO - also apply this to the arm below - // Synchronize the volume from the mixer. This is useful on - // systems that can switch sources from and back to librespot. - let current_volume = self.mixer.volume(); - self.set_volume(current_volume); - self.player.play(); self.state.set_status(PlayStatus::kPlayStatusPlay); self.update_state_position(position_ms); @@ -938,13 +939,16 @@ impl SpircTask { }; } SpircPlayStatus::LoadingPause { position_ms } => { - // TODO - fix "Player::play called from invalid state" when hitting play - // on initial start-up, when starting halfway a track self.player.play(); self.play_status = SpircPlayStatus::LoadingPlay { position_ms }; } - _ => (), + _ => return, } + + // Synchronize the volume from the mixer. This is useful on + // systems that can switch sources from and back to librespot. + let current_volume = self.mixer.volume(); + self.set_volume(current_volume); } fn handle_play_pause(&mut self) { @@ -1252,11 +1256,11 @@ impl SpircTask { } fn update_tracks(&mut self, frame: &protocol::spirc::Frame) { - debug!("State: {:?}", frame.get_state()); + trace!("State: {:#?}", frame.get_state()); let index = frame.get_state().get_playing_track_index(); let context_uri = frame.get_state().get_context_uri().to_owned(); let tracks = frame.get_state().get_track(); - debug!("Frame has {:?} tracks", tracks.len()); + trace!("Frame has {:?} tracks", tracks.len()); if context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:") { diff --git a/playback/src/player.rs b/playback/src/player.rs index 2c5d25c3..747c4967 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,5 +1,6 @@ use std::{ cmp::max, + fmt, future::Future, io::{self, Read, Seek, SeekFrom}, mem, @@ -234,7 +235,16 @@ impl Default for NormalisationData { impl NormalisationData { fn parse_from_file(mut file: T) -> io::Result { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; - file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?; + + let newpos = file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?; + if newpos != SPOTIFY_NORMALIZATION_HEADER_START_OFFSET { + error!( + "NormalisationData::parse_from_file seeking to {} but position is now {}", + SPOTIFY_NORMALIZATION_HEADER_START_OFFSET, newpos + ); + error!("Falling back to default (non-track and non-album) normalisation data."); + return Ok(NormalisationData::default()); + } let track_gain_db = file.read_f32::()?; let track_peak = file.read_f32::()?; @@ -527,7 +537,7 @@ impl PlayerState { Stopped | EndOfTrack { .. } | Paused { .. } | Loading { .. } => false, Playing { .. } => true, Invalid => { - error!("PlayerState is_playing: invalid state"); + error!("PlayerState::is_playing in invalid state"); exit(1); } } @@ -555,7 +565,7 @@ impl PlayerState { ref mut decoder, .. } => Some(decoder), Invalid => { - error!("PlayerState decoder: invalid state"); + error!("PlayerState::decoder in invalid state"); exit(1); } } @@ -574,7 +584,7 @@ impl PlayerState { .. } => Some(stream_loader_controller), Invalid => { - error!("PlayerState stream_loader_controller: invalid state"); + error!("PlayerState::stream_loader_controller in invalid state"); exit(1); } } @@ -582,7 +592,8 @@ impl PlayerState { fn playing_to_end_of_track(&mut self) { use self::PlayerState::*; - match mem::replace(self, Invalid) { + let new_state = mem::replace(self, Invalid); + match new_state { Playing { track_id, play_request_id, @@ -608,7 +619,10 @@ impl PlayerState { }; } _ => { - error!("Called playing_to_end_of_track in non-playing state."); + error!( + "Called playing_to_end_of_track in non-playing state: {:?}", + new_state + ); exit(1); } } @@ -616,7 +630,8 @@ impl PlayerState { fn paused_to_playing(&mut self) { use self::PlayerState::*; - match ::std::mem::replace(self, Invalid) { + let new_state = mem::replace(self, Invalid); + match new_state { Paused { track_id, play_request_id, @@ -644,7 +659,10 @@ impl PlayerState { }; } _ => { - error!("PlayerState paused_to_playing: invalid state"); + error!( + "PlayerState::paused_to_playing in invalid state: {:?}", + new_state + ); exit(1); } } @@ -652,7 +670,8 @@ impl PlayerState { fn playing_to_paused(&mut self) { use self::PlayerState::*; - match ::std::mem::replace(self, Invalid) { + let new_state = mem::replace(self, Invalid); + match new_state { Playing { track_id, play_request_id, @@ -680,7 +699,10 @@ impl PlayerState { }; } _ => { - error!("PlayerState playing_to_paused: invalid state"); + error!( + "PlayerState::playing_to_paused in invalid state: {:?}", + new_state + ); exit(1); } } @@ -900,15 +922,18 @@ impl PlayerTrackLoader { } }; + let mut stream_position_pcm = 0; let position_pcm = PlayerInternal::position_ms_to_pcm(position_ms); - if position_pcm != 0 { - if let Err(e) = decoder.seek(position_pcm) { - error!("PlayerTrackLoader load_track: {}", e); + if position_pcm > 0 { + stream_loader_controller.set_random_access_mode(); + match decoder.seek(position_pcm) { + Ok(_) => stream_position_pcm = position_pcm, + Err(e) => error!("PlayerTrackLoader::load_track error seeking: {}", e), } stream_loader_controller.set_stream_mode(); - } - let stream_position_pcm = position_pcm; + }; + info!("<{}> ({} ms) loaded", audio.name, audio.duration); return Some(PlayerLoadedTrackData { @@ -1237,7 +1262,7 @@ impl PlayerInternal { } PlayerState::Stopped => (), PlayerState::Invalid => { - error!("PlayerInternal handle_player_stop: invalid state"); + error!("PlayerInternal::handle_player_stop in invalid state"); exit(1); } } @@ -1263,7 +1288,7 @@ impl PlayerInternal { }); self.ensure_sink_running(); } else { - error!("Player::play called from invalid state"); + error!("Player::play called from invalid state: {:?}", self.state); } } @@ -1287,7 +1312,7 @@ impl PlayerInternal { duration_ms, }); } else { - error!("Player::pause called from invalid state"); + error!("Player::pause called from invalid state: {:?}", self.state); } } @@ -1548,7 +1573,10 @@ impl PlayerInternal { position_ms, }), PlayerState::Invalid { .. } => { - error!("PlayerInternal handle_command_load: invalid state"); + error!( + "Player::handle_command_load called from invalid state: {:?}", + self.state + ); exit(1); } } @@ -1578,12 +1606,12 @@ impl PlayerInternal { loaded_track .stream_loader_controller .set_random_access_mode(); - if let Err(e) = loaded_track.decoder.seek(position_pcm) { - // This may be blocking. - error!("PlayerInternal handle_command_load: {}", e); + // This may be blocking. + match loaded_track.decoder.seek(position_pcm) { + Ok(_) => loaded_track.stream_position_pcm = position_pcm, + Err(e) => error!("PlayerInternal handle_command_load: {}", e), } loaded_track.stream_loader_controller.set_stream_mode(); - loaded_track.stream_position_pcm = position_pcm; } self.preload = PlayerPreload::None; self.start_playback(track_id, play_request_id, loaded_track, play); @@ -1617,12 +1645,14 @@ impl PlayerInternal { if position_pcm != *stream_position_pcm { stream_loader_controller.set_random_access_mode(); - if let Err(e) = decoder.seek(position_pcm) { - // This may be blocking. - error!("PlayerInternal handle_command_load: {}", e); + // This may be blocking. + match decoder.seek(position_pcm) { + Ok(_) => *stream_position_pcm = position_pcm, + Err(e) => { + error!("PlayerInternal::handle_command_load error seeking: {}", e) + } } stream_loader_controller.set_stream_mode(); - *stream_position_pcm = position_pcm; } // Move the info from the current state into a PlayerLoadedTrackData so we can use @@ -1692,9 +1722,10 @@ impl PlayerInternal { loaded_track .stream_loader_controller .set_random_access_mode(); - if let Err(e) = loaded_track.decoder.seek(position_pcm) { - // This may be blocking - error!("PlayerInternal handle_command_load: {}", e); + // This may be blocking + match loaded_track.decoder.seek(position_pcm) { + Ok(_) => loaded_track.stream_position_pcm = position_pcm, + Err(e) => error!("PlayerInternal handle_command_load: {}", e), } loaded_track.stream_loader_controller.set_stream_mode(); } @@ -1824,10 +1855,10 @@ impl PlayerInternal { *stream_position_pcm = position_pcm; } } - Err(e) => error!("PlayerInternal handle_command_seek: {}", e), + Err(e) => error!("PlayerInternal::handle_command_seek error: {}", e), } } else { - error!("Player::seek called from invalid state"); + error!("Player::seek called from invalid state: {:?}", self.state); } // If we're playing, ensure, that we have enough data leaded to avoid a buffer underrun. @@ -1988,8 +2019,8 @@ impl Drop for PlayerInternal { } } -impl ::std::fmt::Debug for PlayerCommand { - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { +impl fmt::Debug for PlayerCommand { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { PlayerCommand::Load { track_id, @@ -2024,8 +2055,8 @@ impl ::std::fmt::Debug for PlayerCommand { } } -impl ::std::fmt::Debug for PlayerState { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { +impl fmt::Debug for PlayerState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use PlayerState::*; match *self { Stopped => f.debug_struct("Stopped").finish(), @@ -2076,9 +2107,19 @@ struct Subfile { impl Subfile { pub fn new(mut stream: T, offset: u64) -> Subfile { - if let Err(e) = stream.seek(SeekFrom::Start(offset)) { - error!("Subfile new Error: {}", e); + let target = SeekFrom::Start(offset); + match stream.seek(target) { + Ok(pos) => { + if pos != offset { + error!( + "Subfile::new seeking to {:?} but position is now {:?}", + target, pos + ); + } + } + Err(e) => error!("Subfile new Error: {}", e), } + Subfile { stream, offset } } } @@ -2097,10 +2138,14 @@ impl Seek for Subfile { }; let newpos = self.stream.seek(pos)?; - if newpos > self.offset { + + if newpos >= self.offset { Ok(newpos - self.offset) } else { - Ok(0) + Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "newpos < self.offset", + )) } } }