From 1ade02b7ad1c20de775d10c0f0a0c48e0ca25038 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Sat, 22 May 2021 19:05:13 +0200 Subject: [PATCH 001/147] Add basic websocket support --- Cargo.lock | 136 +++++++++ core/Cargo.toml | 3 +- core/src/apresolve.rs | 34 ++- core/src/connection/mod.rs | 48 +-- core/src/dealer/maps.rs | 117 +++++++ core/src/dealer/mod.rs | 586 ++++++++++++++++++++++++++++++++++++ core/src/dealer/protocol.rs | 39 +++ core/src/lib.rs | 11 +- core/src/session.rs | 4 +- core/src/socket.rs | 35 +++ core/src/util.rs | 95 ++++++ 11 files changed, 1040 insertions(+), 68 deletions(-) create mode 100644 core/src/dealer/maps.rs create mode 100644 core/src/dealer/mod.rs create mode 100644 core/src/dealer/protocol.rs create mode 100644 core/src/socket.rs diff --git a/Cargo.lock b/Cargo.lock index 6c0a6fd2..1f97d578 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -918,6 +918,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "input_buffer" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" +dependencies = [ + "bytes", +] + [[package]] name = "instant" version = "0.1.9" @@ -1229,6 +1238,7 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "tokio-tungstenite", "tokio-util", "url", "uuid", @@ -1911,6 +1921,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rodio" version = "0.14.0" @@ -1945,6 +1970,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring", + "sct", + "webpki", +] + [[package]] name = "ryu" version = "1.0.5" @@ -1966,6 +2004,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sdl2" version = "0.34.5" @@ -2103,6 +2151,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stdweb" version = "0.1.3" @@ -2275,6 +2329,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + [[package]] name = "tokio-stream" version = "0.1.5" @@ -2286,6 +2351,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e96bb520beab540ab664bd5a9cfeaa1fcd846fa68c830b42e2c8963071251d2" +dependencies = [ + "futures-util", + "log", + "pin-project", + "rustls", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki", + "webpki-roots", +] + [[package]] name = "tokio-util" version = "0.6.6" @@ -2341,6 +2423,29 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "tungstenite" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe8dada8c1a3aeca77d6b51a4f1314e0f4b8e438b7b1b71e3ddaca8080e4093" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "input_buffer", + "log", + "rand", + "rustls", + "sha-1", + "thiserror", + "url", + "utf-8", + "webpki", + "webpki-roots", +] + [[package]] name = "typenum" version = "1.13.0" @@ -2389,6 +2494,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.2.2" @@ -2401,6 +2512,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "uuid" version = "0.8.2" @@ -2561,6 +2678,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/core/Cargo.toml b/core/Cargo.toml index 80db5687..8ed21273 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -39,8 +39,9 @@ serde_json = "1.0" sha-1 = "0.9" shannon = "0.2.0" thiserror = "1.0.7" -tokio = { version = "1.0", features = ["io-util", "net", "rt", "sync"] } +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"] } tokio-util = { version = "0.6", features = ["codec"] } url = "2.1" uuid = { version = "0.8", default-features = false, features = ["v4"] } diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index b11e275f..8dced22d 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,12 +1,12 @@ use std::error::Error; use hyper::client::HttpConnector; -use hyper::{Body, Client, Method, Request, Uri}; +use hyper::{Body, Client, Method, Request}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use serde::Deserialize; use url::Url; -use super::AP_FALLBACK; +use super::ap_fallback; const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80"; @@ -18,7 +18,7 @@ struct ApResolveData { async fn try_apresolve( proxy: Option<&Url>, ap_port: Option, -) -> Result> { +) -> Result<(String, u16), Box> { let port = ap_port.unwrap_or(443); let mut req = Request::new(Body::empty()); @@ -43,27 +43,29 @@ async fn try_apresolve( let body = hyper::body::to_bytes(response.into_body()).await?; let data: ApResolveData = serde_json::from_slice(body.as_ref())?; + let mut aps = data.ap_list.into_iter().filter_map(|ap| { + let mut split = ap.rsplitn(2, ':'); + let port = split + .next() + .expect("rsplitn should not return empty iterator"); + let host = split.next()?.to_owned(); + let port: u16 = port.parse().ok()?; + Some((host, port)) + }); let ap = if ap_port.is_some() || proxy.is_some() { - data.ap_list.into_iter().find_map(|ap| { - if ap.parse::().ok()?.port()? == port { - Some(ap) - } else { - None - } - }) + aps.find(|(_, p)| *p == port) } else { - data.ap_list.into_iter().next() + aps.next() } - .ok_or("empty AP List")?; + .ok_or("no valid AP in list")?; Ok(ap) } -pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> String { +pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> (String, u16) { try_apresolve(proxy, ap_port).await.unwrap_or_else(|e| { - warn!("Failed to resolve Access Point: {}", e); - warn!("Using fallback \"{}\"", AP_FALLBACK); - AP_FALLBACK.into() + warn!("Failed to resolve Access Point: {}, using fallback.", e); + ap_fallback() }) } diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 58d3e83a..bacdc653 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -5,7 +5,6 @@ pub use self::codec::ApCodec; pub use self::handshake::handshake; use std::io::{self, ErrorKind}; -use std::net::ToSocketAddrs; use futures_util::{SinkExt, StreamExt}; use protobuf::{self, Message, ProtobufError}; @@ -16,7 +15,6 @@ use url::Url; use crate::authentication::Credentials; use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; -use crate::proxytunnel; use crate::version; pub type Transport = Framed; @@ -58,50 +56,8 @@ impl From for AuthenticationError { } } -pub async fn connect(addr: String, proxy: Option<&Url>) -> io::Result { - let socket = if let Some(proxy_url) = proxy { - info!("Using proxy \"{}\"", proxy_url); - - let socket_addr = proxy_url.socket_addrs(|| None).and_then(|addrs| { - addrs.into_iter().next().ok_or_else(|| { - io::Error::new( - io::ErrorKind::NotFound, - "Can't resolve proxy server address", - ) - }) - })?; - let socket = TcpStream::connect(&socket_addr).await?; - - let uri = addr.parse::().map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidData, - "Can't parse access point address", - ) - })?; - let host = uri.host().ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "The access point address contains no hostname", - ) - })?; - let port = uri.port().ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "The access point address contains no port", - ) - })?; - - proxytunnel::proxy_connect(socket, host, port.as_str()).await? - } else { - let socket_addr = addr.to_socket_addrs()?.next().ok_or_else(|| { - io::Error::new( - io::ErrorKind::NotFound, - "Can't resolve access point address", - ) - })?; - - TcpStream::connect(&socket_addr).await? - }; +pub async fn connect(host: &str, port: u16, proxy: Option<&Url>) -> io::Result { + let socket = crate::socket::connect(host, port, proxy).await?; handshake(socket).await } diff --git a/core/src/dealer/maps.rs b/core/src/dealer/maps.rs new file mode 100644 index 00000000..38916e40 --- /dev/null +++ b/core/src/dealer/maps.rs @@ -0,0 +1,117 @@ +use std::collections::HashMap; + +#[derive(Debug)] +pub struct AlreadyHandledError(()); + +pub enum HandlerMap { + Leaf(T), + Branch(HashMap>), +} + +impl Default for HandlerMap { + fn default() -> Self { + Self::Branch(HashMap::new()) + } +} + +impl HandlerMap { + pub fn insert<'a>( + &mut self, + mut path: impl Iterator, + handler: T, + ) -> Result<(), AlreadyHandledError> { + match self { + Self::Leaf(_) => Err(AlreadyHandledError(())), + Self::Branch(children) => { + if let Some(component) = path.next() { + let node = children.entry(component.to_owned()).or_default(); + node.insert(path, handler) + } else if children.is_empty() { + *self = Self::Leaf(handler); + Ok(()) + } else { + Err(AlreadyHandledError(())) + } + } + } + } + + pub fn get<'a>(&self, mut path: impl Iterator) -> Option<&T> { + match self { + Self::Leaf(t) => Some(t), + Self::Branch(m) => { + let component = path.next()?; + m.get(component)?.get(path) + } + } + } + + pub fn remove<'a>(&mut self, mut path: impl Iterator) -> Option { + match self { + Self::Leaf(_) => match std::mem::take(self) { + Self::Leaf(t) => Some(t), + _ => unreachable!(), + }, + Self::Branch(map) => { + let component = path.next()?; + let next = map.get_mut(component)?; + let result = next.remove(path); + match &*next { + Self::Branch(b) if b.is_empty() => { + map.remove(component); + } + _ => (), + } + result + } + } + } +} + +pub struct SubscriberMap { + subscribed: Vec, + children: HashMap>, +} + +impl Default for SubscriberMap { + fn default() -> Self { + Self { + subscribed: Vec::new(), + children: HashMap::new(), + } + } +} + +impl SubscriberMap { + pub fn insert<'a>(&mut self, mut path: impl Iterator, handler: T) { + if let Some(component) = path.next() { + self.children + .entry(component.to_owned()) + .or_default() + .insert(path, handler); + } else { + self.subscribed.push(handler); + } + } + + pub fn is_empty(&self) -> bool { + self.children.is_empty() && self.subscribed.is_empty() + } + + pub fn retain<'a>( + &mut self, + mut path: impl Iterator, + fun: &mut impl FnMut(&T) -> bool, + ) { + self.subscribed.retain(|x| fun(x)); + + if let Some(next) = path.next() { + if let Some(y) = self.children.get_mut(next) { + y.retain(path, fun); + if y.is_empty() { + self.children.remove(next); + } + } + } + } +} diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs new file mode 100644 index 00000000..53cddba0 --- /dev/null +++ b/core/src/dealer/mod.rs @@ -0,0 +1,586 @@ +mod maps; +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 futures_core::{Future, Stream}; +use futures_util::future::join_all; +use futures_util::{SinkExt, StreamExt}; +use tokio::select; +use tokio::sync::mpsc::{self, UnboundedReceiver}; +use tokio::sync::Semaphore; +use tokio::task::JoinHandle; +use tokio_tungstenite::tungstenite; +use tungstenite::error::UrlError; +use url::Url; + +use self::maps::*; +use self::protocol::*; +pub use self::protocol::{Message, Request}; +use crate::socket; +use crate::util::{keep_flushing, CancelOnDrop, TimeoutOnDrop}; + +type WsMessage = tungstenite::Message; +type WsError = tungstenite::Error; +type WsResult = Result; + +pub struct Response { + pub success: bool, +} + +pub struct Responder { + key: String, + tx: mpsc::UnboundedSender, + sent: bool, +} + +impl Responder { + fn new(key: String, tx: mpsc::UnboundedSender) -> Self { + Self { + key, + tx, + sent: false, + } + } + + // Should only be called once + fn send_internal(&mut self, response: Response) { + let response = serde_json::json!({ + "type": "reply", + "key": &self.key, + "payload": { + "success": response.success, + } + }) + .to_string(); + + if let Err(e) = self.tx.send(WsMessage::Text(response)) { + warn!("Wasn't able to reply to dealer request: {}", e); + } + } + + pub fn send(mut self, success: Response) { + self.send_internal(success); + self.sent = true; + } + + pub fn force_unanswered(mut self) { + self.sent = true; + } +} + +impl Drop for Responder { + fn drop(&mut self) { + if !self.sent { + self.send_internal(Response { success: false }); + } + } +} + +pub trait IntoResponse { + fn respond(self, responder: Responder); +} + +impl IntoResponse for Response { + fn respond(self, responder: Responder) { + responder.send(self) + } +} + +impl IntoResponse for F +where + F: Future + Send + 'static, +{ + fn respond(self, responder: Responder) { + tokio::spawn(async move { + responder.send(self.await); + }); + } +} + +impl RequestHandler for F +where + F: (Fn(Request) -> R) + Send + Sync + 'static, + R: IntoResponse, +{ + fn handle_request(&self, request: Request, responder: Responder) { + self(request).respond(responder); + } +} + +pub trait RequestHandler: Send + Sync + 'static { + fn handle_request(&self, request: Request, responder: Responder); +} + +type MessageHandler = mpsc::UnboundedSender>; + +// TODO: Maybe it's possible to unregister subscription directly when they +// are dropped instead of on next failed attempt. +pub struct Subscription(UnboundedReceiver>); + +impl Stream for Subscription { + type Item = Message; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.0.poll_recv(cx) + } +} + +fn split_uri(s: &str) -> Option> { + let (scheme, sep, rest) = if let Some(rest) = s.strip_prefix("hm://") { + ("hm", '/', rest) + } else if let Some(rest) = s.strip_suffix("spotify:") { + ("spotify", ':', rest) + } else { + return None; + }; + + let rest = rest.trim_end_matches(sep); + let mut split = rest.split(sep); + + if rest.is_empty() { + assert_eq!(split.next(), Some("")); + } + + Some(iter::once(scheme).chain(split)) +} + +#[derive(Debug, Clone)] +pub enum AddHandlerError { + AlreadyHandled, + InvalidUri, +} + +#[derive(Debug, Clone)] +pub enum SubscriptionError { + InvalidUri, +} + +fn add_handler( + map: &mut HandlerMap>, + uri: &str, + handler: H, +) -> Result<(), AddHandlerError> +where + H: RequestHandler, +{ + let split = split_uri(uri).ok_or(AddHandlerError::InvalidUri)?; + map.insert(split, Box::new(handler)) + .map_err(|_| AddHandlerError::AlreadyHandled) +} + +fn remove_handler(map: &mut HandlerMap, uri: &str) -> Option { + map.remove(split_uri(uri)?) +} + +fn subscribe( + map: &mut SubscriberMap, + uris: &[&str], +) -> Result { + let (tx, rx) = mpsc::unbounded_channel(); + + for &uri in uris { + let split = split_uri(uri).ok_or(SubscriptionError::InvalidUri)?; + map.insert(split, tx.clone()); + } + + Ok(Subscription(rx)) +} + +#[derive(Default)] +pub struct Builder { + message_handlers: SubscriberMap, + request_handlers: HandlerMap>, +} + +macro_rules! create_dealer { + ($builder:expr, $shared:ident -> $body:expr) => { + match $builder { + builder => { + let shared = Arc::new(DealerShared { + message_handlers: Mutex::new(builder.message_handlers), + request_handlers: Mutex::new(builder.request_handlers), + notify_drop: Semaphore::new(0), + }); + + let handle = { + let $shared = Arc::clone(&shared); + tokio::spawn($body) + }; + + Dealer { + shared, + handle: TimeoutOnDrop::new(handle, Duration::from_secs(3)), + } + } + } + }; +} + +impl Builder { + pub fn new() -> Self { + Self::default() + } + + pub fn add_handler( + &mut self, + uri: &str, + handler: impl RequestHandler, + ) -> Result<(), AddHandlerError> { + add_handler(&mut self.request_handlers, uri, handler) + } + + pub fn subscribe(&mut self, uris: &[&str]) -> Result { + subscribe(&mut self.message_handlers, uris) + } + + pub fn launch_in_background(self, get_url: F, proxy: Option) -> Dealer + where + Fut: Future + Send + 'static, + F: (FnMut() -> Fut) + Send + 'static, + { + create_dealer!(self, shared -> run(shared, None, get_url, proxy)) + } + + pub async fn launch(self, mut get_url: F, proxy: Option) -> WsResult + where + Fut: Future + Send + 'static, + F: (FnMut() -> Fut) + Send + 'static, + { + let dealer = create_dealer!(self, shared -> { + // Try to connect. + let url = get_url().await; + let tasks = connect(&url, proxy.as_ref(), &shared).await?; + + // If a connection is established, continue in a background task. + run(shared, Some(tasks), get_url, proxy) + }); + + Ok(dealer) + } +} + +struct DealerShared { + message_handlers: Mutex>, + request_handlers: Mutex>>, + + // Semaphore with 0 permits. By closing this semaphore, we indicate + // that the actual Dealer struct has been dropped. + notify_drop: Semaphore, +} + +impl DealerShared { + fn dispatch_message(&self, msg: Message) { + if let Some(split) = split_uri(&msg.uri) { + self.message_handlers + .lock() + .unwrap() + .retain(split, &mut |tx| tx.send(msg.clone()).is_ok()); + } + } + + fn dispatch_request( + &self, + request: Request, + send_tx: &mpsc::UnboundedSender, + ) { + // ResponseSender will automatically send "success: false" if it is dropped without an answer. + let responder = Responder::new(request.key.clone(), send_tx.clone()); + + let split = if let Some(split) = split_uri(&request.message_ident) { + split + } else { + warn!( + "Dealer request with invalid message_ident: {}", + &request.message_ident + ); + return; + }; + + { + let handler_map = self.request_handlers.lock().unwrap(); + + if let Some(handler) = handler_map.get(split) { + handler.handle_request(request, responder); + return; + } + } + + warn!("No handler for message_ident: {}", &request.message_ident); + } + + fn dispatch(&self, m: MessageOrRequest, send_tx: &mpsc::UnboundedSender) { + match m { + MessageOrRequest::Message(m) => self.dispatch_message(m), + MessageOrRequest::Request(r) => self.dispatch_request(r, send_tx), + } + } + + async fn closed(&self) { + self.notify_drop.acquire().await.unwrap_err(); + } + + fn is_closed(&self) -> bool { + self.notify_drop.is_closed() + } +} + +pub struct Dealer { + shared: Arc, + handle: TimeoutOnDrop<()>, +} + +impl Dealer { + pub fn add_handler(&self, uri: &str, handler: H) -> Result<(), AddHandlerError> + where + H: RequestHandler, + { + add_handler( + &mut self.shared.request_handlers.lock().unwrap(), + uri, + handler, + ) + } + + pub fn remove_handler(&self, uri: &str) -> Option> { + remove_handler(&mut self.shared.request_handlers.lock().unwrap(), uri) + } + + pub fn subscribe(&self, uris: &[&str]) -> Result { + subscribe(&mut self.shared.message_handlers.lock().unwrap(), uris) + } + + pub async fn close(mut self) { + debug!("closing dealer"); + + self.shared.notify_drop.close(); + + if let Some(handle) = self.handle.take() { + CancelOnDrop(handle).await.unwrap(); + } + } +} + +/// Initializes a connection and returns futures that will finish when the connection is closed/lost. +async fn connect( + address: &Url, + proxy: Option<&Url>, + shared: &Arc, +) -> WsResult<(JoinHandle<()>, JoinHandle<()>)> { + let host = address + .host_str() + .ok_or(WsError::Url(UrlError::NoHostName))?; + + let default_port = match address.scheme() { + "ws" => 80, + "wss" => 443, + _ => return Err(WsError::Url(UrlError::UnsupportedUrlScheme)), + }; + + let port = address.port().unwrap_or(default_port); + + let stream = socket::connect(host, port, proxy).await?; + + let (mut ws_tx, ws_rx) = tokio_tungstenite::client_async_tls(address, stream) + .await? + .0 + .split(); + + let (send_tx, mut send_rx) = mpsc::unbounded_channel::(); + + // Spawn a task that will forward messages from the channel to the websocket. + let send_task = { + let shared = Arc::clone(&shared); + + tokio::spawn(async move { + let result = loop { + select! { + biased; + () = shared.closed() => { + break Ok(None); + } + msg = send_rx.recv() => { + if let Some(msg) = msg { + // New message arrived through channel + if let WsMessage::Close(close_frame) = msg { + break Ok(close_frame); + } + + if let Err(e) = ws_tx.feed(msg).await { + break Err(e); + } + } else { + break Ok(None); + } + }, + e = keep_flushing(&mut ws_tx) => { + break Err(e) + } + } + }; + + send_rx.close(); + + // I don't trust in tokio_tungstenite's implementation of Sink::close. + let result = match result { + Ok(close_frame) => ws_tx.send(WsMessage::Close(close_frame)).await, + Err(WsError::AlreadyClosed) | Err(WsError::ConnectionClosed) => ws_tx.flush().await, + Err(e) => { + warn!("Dealer finished with an error: {}", e); + ws_tx.send(WsMessage::Close(None)).await + } + }; + + if let Err(e) = result { + warn!("Error while closing websocket: {}", e); + } + + debug!("Dropping send task"); + }) + }; + + let shared = Arc::clone(&shared); + + // A task that receives messages from the web socket. + let receive_task = tokio::spawn(async { + let pong_received = AtomicBool::new(true); + let send_tx = send_tx; + let shared = shared; + + let receive_task = async { + let mut ws_rx = ws_rx; + + loop { + match ws_rx.next().await { + Some(Ok(msg)) => match msg { + WsMessage::Text(t) => match serde_json::from_str(&t) { + Ok(m) => shared.dispatch(m, &send_tx), + Err(e) => info!("Received invalid message: {}", e), + }, + WsMessage::Binary(_) => { + info!("Received invalid binary message"); + } + WsMessage::Pong(_) => { + debug!("Received pong"); + pong_received.store(true, atomic::Ordering::Relaxed); + } + _ => (), // tungstenite handles Close and Ping automatically + }, + Some(Err(e)) => { + warn!("Websocket connection failed: {}", e); + break; + } + None => { + debug!("Websocket connection closed."); + break; + } + } + } + }; + + // Sends pings and checks whether a pong comes back. + let ping_task = async { + use tokio::time::{interval, sleep}; + + let mut timer = interval(Duration::from_secs(30)); + + loop { + timer.tick().await; + + pong_received.store(false, atomic::Ordering::Relaxed); + if send_tx.send(WsMessage::Ping(vec![])).is_err() { + // The sender is closed. + break; + } + + debug!("Sent ping"); + + sleep(Duration::from_secs(3)).await; + + if !pong_received.load(atomic::Ordering::SeqCst) { + // No response + warn!("Websocket peer does not respond."); + break; + } + } + }; + + // Exit this task as soon as one our subtasks fails. + // In both cases the connection is probably lost. + select! { + () = ping_task => (), + () = receive_task => () + } + + // Try to take send_task down with us, in case it's still alive. + let _ = send_tx.send(WsMessage::Close(None)); + + debug!("Dropping receive task"); + }); + + Ok((send_task, receive_task)) +} + +/// The main background task for `Dealer`, which coordinates reconnecting. +async fn run( + shared: Arc, + initial_tasks: Option<(JoinHandle<()>, JoinHandle<()>)>, + mut get_url: F, + proxy: Option, +) where + Fut: Future + Send + 'static, + F: (FnMut() -> Fut) + Send + 'static, +{ + let init_task = |t| Some(TimeoutOnDrop::new(t, Duration::from_secs(3))); + + let mut tasks = if let Some((s, r)) = initial_tasks { + (init_task(s), init_task(r)) + } else { + (None, None) + }; + + while !shared.is_closed() { + match &mut tasks { + (Some(t0), Some(t1)) => { + select! { + () = shared.closed() => break, + r = t0 => { + r.unwrap(); // Whatever has gone wrong (probably panicked), we can't handle it, so let's panic too. + tasks.0.take(); + }, + r = t1 => { + r.unwrap(); + tasks.1.take(); + } + } + } + _ => { + let url = select! { + () = shared.closed() => { + break + }, + e = get_url() => e + }; + + 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); + } + } + } + } + } + + let tasks = tasks.0.into_iter().chain(tasks.1); + + let _ = join_all(tasks).await; +} diff --git a/core/src/dealer/protocol.rs b/core/src/dealer/protocol.rs new file mode 100644 index 00000000..cb0a1835 --- /dev/null +++ b/core/src/dealer/protocol.rs @@ -0,0 +1,39 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +pub type JsonValue = serde_json::Value; +pub type JsonObject = serde_json::Map; + +#[derive(Clone, Debug, Deserialize)] +pub struct Request

{ + #[serde(default)] + pub headers: HashMap, + pub message_ident: String, + pub key: String, + pub payload: P, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Payload { + pub message_id: i32, + pub sent_by_device_id: String, + pub command: JsonObject, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Message

{ + #[serde(default)] + pub headers: HashMap, + pub method: Option, + #[serde(default)] + pub payloads: Vec

, + pub uri: String, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum MessageOrRequest { + Message(Message), + Request(Request), +} diff --git a/core/src/lib.rs b/core/src/lib.rs index bb3e21d5..c6f6e190 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -14,25 +14,30 @@ pub mod cache; pub mod channel; pub mod config; mod connection; +#[allow(dead_code)] +mod dealer; #[doc(hidden)] pub mod diffie_hellman; pub mod keymaster; pub mod mercury; mod proxytunnel; pub mod session; +mod socket; pub mod spotify_id; #[doc(hidden)] pub mod util; pub mod version; -const AP_FALLBACK: &str = "ap.spotify.com:443"; +fn ap_fallback() -> (String, u16) { + (String::from("ap.spotify.com"), 443) +} #[cfg(feature = "apresolve")] mod apresolve; #[cfg(not(feature = "apresolve"))] mod apresolve { - pub async fn apresolve(_: Option<&url::Url>, _: Option) -> String { - return super::AP_FALLBACK.into(); + pub async fn apresolve(_: Option<&url::Url>, _: Option) -> (String, u16) { + super::ap_fallback() } } diff --git a/core/src/session.rs b/core/src/session.rs index 6c4abc54..f43a4cc0 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -69,8 +69,8 @@ impl Session { ) -> Result { let ap = apresolve(config.proxy.as_ref(), config.ap_port).await; - info!("Connecting to AP \"{}\"", ap); - let mut conn = connection::connect(ap, config.proxy.as_ref()).await?; + info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); + let mut conn = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?; let reusable_credentials = connection::authenticate(&mut conn, credentials, &config.device_id).await?; diff --git a/core/src/socket.rs b/core/src/socket.rs new file mode 100644 index 00000000..92274cc6 --- /dev/null +++ b/core/src/socket.rs @@ -0,0 +1,35 @@ +use std::io; +use std::net::ToSocketAddrs; + +use tokio::net::TcpStream; +use url::Url; + +use crate::proxytunnel; + +pub async fn connect(host: &str, port: u16, proxy: Option<&Url>) -> io::Result { + let socket = if let Some(proxy_url) = proxy { + info!("Using proxy \"{}\"", proxy_url); + + let socket_addr = proxy_url.socket_addrs(|| None).and_then(|addrs| { + addrs.into_iter().next().ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "Can't resolve proxy server address", + ) + }) + })?; + let socket = TcpStream::connect(&socket_addr).await?; + + proxytunnel::proxy_connect(socket, host, &port.to_string()).await? + } else { + let socket_addr = (host, port).to_socket_addrs()?.next().ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "Can't resolve access point address", + ) + })?; + + TcpStream::connect(&socket_addr).await? + }; + Ok(socket) +} diff --git a/core/src/util.rs b/core/src/util.rs index df9ea714..4f78c467 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,4 +1,99 @@ +use std::future::Future; use std::mem; +use std::pin::Pin; +use std::task::Context; +use std::task::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; + +/// Returns a future that will flush the sink, even if flushing is temporarily completed. +/// Finishes only if the sink throws an error. +pub(crate) fn keep_flushing<'a, T, S: Sink + Unpin + 'a>( + mut s: S, +) -> impl Future + 'a { + future::poll_fn(move |cx| match s.poll_flush_unpin(cx) { + Poll::Ready(Err(e)) => Poll::Ready(e), + _ => Poll::Pending, + }) +} + +pub struct CancelOnDrop(pub JoinHandle); + +impl Future for CancelOnDrop { + type Output = as Future>::Output; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.0.poll_unpin(cx) + } +} + +impl Drop for CancelOnDrop { + fn drop(&mut self) { + self.0.abort(); + } +} + +pub struct TimeoutOnDrop { + handle: Option>, + timeout: tokio::time::Duration, +} + +impl TimeoutOnDrop { + pub fn new(handle: JoinHandle, timeout: tokio::time::Duration) -> Self { + Self { + handle: Some(handle), + timeout, + } + } + + pub fn take(&mut self) -> Option> { + self.handle.take() + } +} + +impl Future for TimeoutOnDrop { + type Output = as Future>::Output; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let r = ready!(self + .handle + .as_mut() + .expect("Polled after ready") + .poll_unpin(cx)); + self.handle = None; + Poll::Ready(r) + } +} + +impl Drop for TimeoutOnDrop { + fn drop(&mut self) { + let mut handle = if let Some(handle) = self.handle.take() { + handle + } else { + return; + }; + + if (&mut handle).now_or_never().is_some() { + // Already finished + return; + } + + match tokio::runtime::Handle::try_current() { + Ok(h) => { + h.spawn(timeout(self.timeout, CancelOnDrop(handle))); + } + Err(_) => { + // Not in tokio context, can't spawn + handle.abort(); + } + } + } +} pub trait Seq { fn next(&self) -> Self; From 7ed35396f85e16dbe939841c05edd37ec05f863e Mon Sep 17 00:00:00 2001 From: Johannesd3 <51954457+Johannesd3@users.noreply.github.com> Date: Thu, 27 May 2021 15:33:29 +0200 Subject: [PATCH 002/147] Mostly cosmetic changes in `dealer` (#762) * Add missing timeout on reconnect * Cosmetic changes --- core/src/dealer/mod.rs | 62 ++++++++++++++++++++----------------- core/src/dealer/protocol.rs | 28 ++++++++--------- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index 53cddba0..bca1ec20 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -1,5 +1,5 @@ mod maps; -mod protocol; +pub mod protocol; use std::iter; use std::pin::Pin; @@ -11,6 +11,7 @@ use std::time::Duration; use futures_core::{Future, Stream}; use futures_util::future::join_all; use futures_util::{SinkExt, StreamExt}; +use thiserror::Error; use tokio::select; use tokio::sync::mpsc::{self, UnboundedReceiver}; use tokio::sync::Semaphore; @@ -21,7 +22,6 @@ use url::Url; use self::maps::*; use self::protocol::*; -pub use self::protocol::{Message, Request}; use crate::socket; use crate::util::{keep_flushing, CancelOnDrop, TimeoutOnDrop}; @@ -29,6 +29,13 @@ type WsMessage = tungstenite::Message; type WsError = tungstenite::Error; type WsResult = Result; +const WEBSOCKET_CLOSE_TIMEOUT: Duration = Duration::from_secs(3); + +const PING_INTERVAL: Duration = Duration::from_secs(30); +const PING_TIMEOUT: Duration = Duration::from_secs(3); + +const RECONNECT_INTERVAL: Duration = Duration::from_secs(10); + pub struct Response { pub success: bool, } @@ -64,8 +71,8 @@ impl Responder { } } - pub fn send(mut self, success: Response) { - self.send_internal(success); + pub fn send(mut self, response: Response) { + self.send_internal(response); self.sent = true; } @@ -105,26 +112,26 @@ where impl RequestHandler for F where - F: (Fn(Request) -> R) + Send + Sync + 'static, + F: (Fn(Request) -> R) + Send + 'static, R: IntoResponse, { - fn handle_request(&self, request: Request, responder: Responder) { + fn handle_request(&self, request: Request, responder: Responder) { self(request).respond(responder); } } -pub trait RequestHandler: Send + Sync + 'static { - fn handle_request(&self, request: Request, responder: Responder); +pub trait RequestHandler: Send + 'static { + fn handle_request(&self, request: Request, responder: Responder); } -type MessageHandler = mpsc::UnboundedSender>; +type MessageHandler = mpsc::UnboundedSender; // TODO: Maybe it's possible to unregister subscription directly when they // are dropped instead of on next failed attempt. -pub struct Subscription(UnboundedReceiver>); +pub struct Subscription(UnboundedReceiver); impl Stream for Subscription { - type Item = Message; + type Item = Message; fn poll_next( mut self: Pin<&mut Self>, @@ -153,25 +160,25 @@ fn split_uri(s: &str) -> Option> { Some(iter::once(scheme).chain(split)) } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Error)] pub enum AddHandlerError { + #[error("There is already a handler for the given uri")] AlreadyHandled, + #[error("The specified uri is invalid")] InvalidUri, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Error)] pub enum SubscriptionError { + #[error("The specified uri is invalid")] InvalidUri, } -fn add_handler( +fn add_handler( map: &mut HandlerMap>, uri: &str, - handler: H, -) -> Result<(), AddHandlerError> -where - H: RequestHandler, -{ + handler: impl RequestHandler, +) -> Result<(), AddHandlerError> { let split = split_uri(uri).ok_or(AddHandlerError::InvalidUri)?; map.insert(split, Box::new(handler)) .map_err(|_| AddHandlerError::AlreadyHandled) @@ -218,7 +225,7 @@ macro_rules! create_dealer { Dealer { shared, - handle: TimeoutOnDrop::new(handle, Duration::from_secs(3)), + handle: TimeoutOnDrop::new(handle, WEBSOCKET_CLOSE_TIMEOUT), } } } @@ -278,7 +285,7 @@ struct DealerShared { } impl DealerShared { - fn dispatch_message(&self, msg: Message) { + fn dispatch_message(&self, msg: Message) { if let Some(split) = split_uri(&msg.uri) { self.message_handlers .lock() @@ -287,11 +294,7 @@ impl DealerShared { } } - fn dispatch_request( - &self, - request: Request, - send_tx: &mpsc::UnboundedSender, - ) { + fn dispatch_request(&self, request: Request, send_tx: &mpsc::UnboundedSender) { // ResponseSender will automatically send "success: false" if it is dropped without an answer. let responder = Responder::new(request.key.clone(), send_tx.clone()); @@ -490,7 +493,7 @@ async fn connect( let ping_task = async { use tokio::time::{interval, sleep}; - let mut timer = interval(Duration::from_secs(30)); + let mut timer = interval(PING_INTERVAL); loop { timer.tick().await; @@ -503,7 +506,7 @@ async fn connect( debug!("Sent ping"); - sleep(Duration::from_secs(3)).await; + sleep(PING_TIMEOUT).await; if !pong_received.load(atomic::Ordering::SeqCst) { // No response @@ -539,7 +542,7 @@ async fn run( Fut: Future + Send + 'static, F: (FnMut() -> Fut) + Send + 'static, { - let init_task = |t| Some(TimeoutOnDrop::new(t, Duration::from_secs(3))); + let init_task = |t| Some(TimeoutOnDrop::new(t, WEBSOCKET_CLOSE_TIMEOUT)); let mut tasks = if let Some((s, r)) = initial_tasks { (init_task(s), init_task(r)) @@ -574,6 +577,7 @@ async fn run( Ok((s, r)) => tasks = (init_task(s), init_task(r)), Err(e) => { warn!("Error while connecting: {}", e); + tokio::time::sleep(RECONNECT_INTERVAL).await; } } } diff --git a/core/src/dealer/protocol.rs b/core/src/dealer/protocol.rs index cb0a1835..9e62a2e5 100644 --- a/core/src/dealer/protocol.rs +++ b/core/src/dealer/protocol.rs @@ -5,15 +5,6 @@ use serde::Deserialize; pub type JsonValue = serde_json::Value; pub type JsonObject = serde_json::Map; -#[derive(Clone, Debug, Deserialize)] -pub struct Request

{ - #[serde(default)] - pub headers: HashMap, - pub message_ident: String, - pub key: String, - pub payload: P, -} - #[derive(Clone, Debug, Deserialize)] pub struct Payload { pub message_id: i32, @@ -22,18 +13,27 @@ pub struct Payload { } #[derive(Clone, Debug, Deserialize)] -pub struct Message

{ +pub struct Request { + #[serde(default)] + pub headers: HashMap, + pub message_ident: String, + pub key: String, + pub payload: Payload, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Message { #[serde(default)] pub headers: HashMap, pub method: Option, #[serde(default)] - pub payloads: Vec

, + pub payloads: Vec, pub uri: String, } #[derive(Clone, Debug, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] -pub enum MessageOrRequest { - Message(Message), - Request(Request), +pub(super) enum MessageOrRequest { + Message(Message), + Request(Request), } From 6244515879d6a4e40fff64a7cb281fff19d069f8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 10 Jun 2021 22:24:40 +0200 Subject: [PATCH 003/147] Resolve `dealer` and `spclient` access points (#795) --- Cargo.toml | 1 - core/Cargo.toml | 7 +-- core/src/apresolve.rs | 121 ++++++++++++++++++++++++++++-------------- core/src/lib.rs | 15 +----- core/src/session.rs | 4 +- 5 files changed, 86 insertions(+), 62 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 081cacae..5df27872 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,6 @@ version = "0.2.0" [dependencies.librespot-core] path = "core" version = "0.2.0" -features = ["apresolve"] [dependencies.librespot-metadata] path = "metadata" diff --git a/core/Cargo.toml b/core/Cargo.toml index 8ed21273..7eb4051c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,8 +23,8 @@ futures-util = { version = "0.3", default-features = false, features = ["alloc", hmac = "0.11" httparse = "1.3" http = "0.2" -hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] } -hyper-proxy = { version = "0.9.1", optional = true, default-features = false } +hyper = { version = "0.14", features = ["client", "tcp", "http1"] } +hyper-proxy = { version = "0.9.1", default-features = false } log = "0.4" num-bigint = { version = "0.4", features = ["rand"] } num-integer = "0.1" @@ -53,6 +53,3 @@ vergen = "3.0.4" [dev-dependencies] env_logger = "0.8" tokio = {version = "1.0", features = ["macros"] } - -[features] -apresolve = ["hyper", "hyper-proxy"] diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 8dced22d..975e0e18 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,30 +1,67 @@ use std::error::Error; use hyper::client::HttpConnector; -use hyper::{Body, Client, Method, Request}; +use hyper::{Body, Client, Request}; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use serde::Deserialize; use url::Url; -use super::ap_fallback; +const APRESOLVE_ENDPOINT: &str = + "http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient"; -const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80"; +// These addresses probably do some geo-location based traffic management or at least DNS-based +// load balancing. They are known to fail when the normal resolvers are up, so that's why they +// should only be used as fallback. +const AP_FALLBACK: &str = "ap.spotify.com"; +const DEALER_FALLBACK: &str = "dealer.spotify.com"; +const SPCLIENT_FALLBACK: &str = "spclient.wg.spotify.com"; -#[derive(Clone, Debug, Deserialize)] +const FALLBACK_PORT: u16 = 443; + +pub type SocketAddress = (String, u16); + +#[derive(Clone, Debug, Default, Deserialize)] struct ApResolveData { - ap_list: Vec, + accesspoint: Vec, + dealer: Vec, + spclient: Vec, } -async fn try_apresolve( - proxy: Option<&Url>, - ap_port: Option, -) -> Result<(String, u16), Box> { - let port = ap_port.unwrap_or(443); +#[derive(Clone, Debug, Deserialize)] +pub struct AccessPoints { + pub accesspoint: SocketAddress, + pub dealer: SocketAddress, + pub spclient: SocketAddress, +} - let mut req = Request::new(Body::empty()); - *req.method_mut() = Method::GET; - // panic safety: APRESOLVE_ENDPOINT above is valid url. - *req.uri_mut() = APRESOLVE_ENDPOINT.parse().expect("invalid AP resolve URL"); +fn select_ap(data: Vec, fallback: &str, ap_port: Option) -> SocketAddress { + let port = ap_port.unwrap_or(FALLBACK_PORT); + + let mut aps = data.into_iter().filter_map(|ap| { + let mut split = ap.rsplitn(2, ':'); + let port = split + .next() + .expect("rsplitn should not return empty iterator"); + let host = split.next()?.to_owned(); + let port: u16 = port.parse().ok()?; + Some((host, port)) + }); + + let ap = if ap_port.is_some() { + aps.find(|(_, p)| *p == port) + } else { + aps.next() + }; + + ap.unwrap_or_else(|| (String::from(fallback), port)) +} + +async fn try_apresolve(proxy: Option<&Url>) -> Result> { + let req = Request::builder() + .method("GET") + .uri(APRESOLVE_ENDPOINT) + .body(Body::empty()) + .unwrap(); let response = if let Some(url) = proxy { // Panic safety: all URLs are valid URIs @@ -43,51 +80,53 @@ async fn try_apresolve( let body = hyper::body::to_bytes(response.into_body()).await?; let data: ApResolveData = serde_json::from_slice(body.as_ref())?; - let mut aps = data.ap_list.into_iter().filter_map(|ap| { - let mut split = ap.rsplitn(2, ':'); - let port = split - .next() - .expect("rsplitn should not return empty iterator"); - let host = split.next()?.to_owned(); - let port: u16 = port.parse().ok()?; - Some((host, port)) - }); - let ap = if ap_port.is_some() || proxy.is_some() { - aps.find(|(_, p)| *p == port) - } else { - aps.next() - } - .ok_or("no valid AP in list")?; - - Ok(ap) + Ok(data) } -pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> (String, u16) { - try_apresolve(proxy, ap_port).await.unwrap_or_else(|e| { - warn!("Failed to resolve Access Point: {}, using fallback.", e); - ap_fallback() - }) +pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> AccessPoints { + let data = try_apresolve(proxy).await.unwrap_or_else(|e| { + warn!("Failed to resolve access points: {}, using fallbacks.", e); + ApResolveData::default() + }); + + let accesspoint = select_ap(data.accesspoint, AP_FALLBACK, ap_port); + let dealer = select_ap(data.dealer, DEALER_FALLBACK, ap_port); + let spclient = select_ap(data.spclient, SPCLIENT_FALLBACK, ap_port); + + AccessPoints { + accesspoint, + dealer, + spclient, + } } #[cfg(test)] mod test { use std::net::ToSocketAddrs; - use super::try_apresolve; + use super::apresolve; #[tokio::test] async fn test_apresolve() { - let ap = try_apresolve(None, None).await.unwrap(); + let aps = apresolve(None, None).await; // Assert that the result contains a valid host and port - ap.to_socket_addrs().unwrap().next().unwrap(); + aps.accesspoint.to_socket_addrs().unwrap().next().unwrap(); + aps.dealer.to_socket_addrs().unwrap().next().unwrap(); + aps.spclient.to_socket_addrs().unwrap().next().unwrap(); } #[tokio::test] async fn test_apresolve_port_443() { - let ap = try_apresolve(None, Some(443)).await.unwrap(); + let aps = apresolve(None, Some(443)).await; - let port = ap.to_socket_addrs().unwrap().next().unwrap().port(); + let port = aps + .accesspoint + .to_socket_addrs() + .unwrap() + .next() + .unwrap() + .port(); assert_eq!(port, 443); } } diff --git a/core/src/lib.rs b/core/src/lib.rs index c6f6e190..f26caf3d 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -8,6 +8,7 @@ use librespot_protocol as protocol; #[macro_use] mod component; +mod apresolve; pub mod audio_key; pub mod authentication; pub mod cache; @@ -27,17 +28,3 @@ pub mod spotify_id; #[doc(hidden)] pub mod util; pub mod version; - -fn ap_fallback() -> (String, u16) { - (String::from("ap.spotify.com"), 443) -} - -#[cfg(feature = "apresolve")] -mod apresolve; - -#[cfg(not(feature = "apresolve"))] -mod apresolve { - pub async fn apresolve(_: Option<&url::Url>, _: Option) -> (String, u16) { - super::ap_fallback() - } -} diff --git a/core/src/session.rs b/core/src/session.rs index f43a4cc0..17452b20 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -67,7 +67,9 @@ impl Session { credentials: Credentials, cache: Option, ) -> Result { - let ap = apresolve(config.proxy.as_ref(), config.ap_port).await; + let ap = apresolve(config.proxy.as_ref(), config.ap_port) + .await + .accesspoint; info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); let mut conn = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?; From 113ac94c07cd6cbec80d47c112b8728bb6128571 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 19 Jun 2021 22:29:48 +0200 Subject: [PATCH 004/147] Update protobufs (#796) * Import Spotify 1.1.61.583 (Windows) protobufs * Import Spotify 1.1.33.569 protobufs missing in 1.1.61.583 * Remove unused protobufs, no longer present in 1.1.61.583 --- metadata/src/lib.rs | 6 +- playback/src/player.rs | 9 +- protocol/build.rs | 3 +- protocol/proto/AdDecisionEvent.proto | 13 + protocol/proto/AdError.proto | 15 + protocol/proto/AdEvent.proto | 27 ++ protocol/proto/AdRequestEvent.proto | 14 + protocol/proto/AdSlotEvent.proto | 19 + protocol/proto/AmazonWakeUpTime.proto | 11 + protocol/proto/AudioDriverError.proto | 14 + protocol/proto/AudioDriverInfo.proto | 14 + protocol/proto/AudioFileSelection.proto | 16 + .../proto/AudioOffliningSettingsReport.proto | 15 + protocol/proto/AudioRateLimit.proto | 18 + protocol/proto/AudioSessionEvent.proto | 13 + protocol/proto/AudioSettingsReport.proto | 30 ++ .../proto/AudioStreamingSettingsReport.proto | 16 + .../BoomboxPlaybackInstrumentation.proto | 18 + protocol/proto/BrokenObject.proto | 14 + protocol/proto/CacheError.proto | 16 + protocol/proto/CachePruningReport.proto | 25 + protocol/proto/CacheRealmPruningReport.proto | 23 + protocol/proto/CacheRealmReport.proto | 18 + protocol/proto/CacheReport.proto | 30 ++ protocol/proto/ClientLocale.proto | 12 + protocol/proto/ColdStartupSequence.proto | 19 + protocol/proto/CollectionLevelDbInfo.proto | 16 + ...ctionOfflineControllerEmptyTrackList.proto | 13 + protocol/proto/ConfigurationApplied.proto | 18 + protocol/proto/ConfigurationFetched.proto | 31 ++ .../proto/ConfigurationFetchedNonAuth.proto | 31 ++ .../proto/ConnectCredentialsRequest.proto | 12 + protocol/proto/ConnectDeviceDiscovered.proto | 22 + protocol/proto/ConnectDialError.proto | 15 + .../proto/ConnectMdnsPacketParseError.proto | 17 + protocol/proto/ConnectPullFailure.proto | 13 + protocol/proto/ConnectTransferResult.proto | 29 ++ protocol/proto/ConnectionError.proto | 13 + protocol/proto/ConnectionInfo.proto | 18 + .../proto/DefaultConfigurationApplied.proto | 17 + .../DesktopAuthenticationFailureNonAuth.proto | 13 + .../proto/DesktopAuthenticationSuccess.proto | 11 + .../proto/DesktopGPUAccelerationInfo.proto | 11 + protocol/proto/DesktopHighMemoryUsage.proto | 19 + .../proto/DesktopUpdateDownloadComplete.proto | 15 + .../proto/DesktopUpdateDownloadError.proto | 15 + .../proto/DesktopUpdateMessageAction.proto | 18 + .../proto/DesktopUpdateMessageProcessed.proto | 16 + protocol/proto/DesktopUpdateResponse.proto | 15 + protocol/proto/Download.proto | 53 ++ protocol/proto/DrmRequestFailure.proto | 14 + protocol/proto/EndAd.proto | 34 ++ .../EventSenderInternalErrorNonAuth.proto | 14 + protocol/proto/EventSenderStats.proto | 13 + protocol/proto/ExternalDeviceInfo.proto | 20 + protocol/proto/GetInfoFailures.proto | 15 + protocol/proto/HeadFileDownload.proto | 26 + protocol/proto/LocalFileSyncError.proto | 11 + protocol/proto/LocalFilesError.proto | 12 + protocol/proto/LocalFilesImport.proto | 15 + protocol/proto/LocalFilesReport.proto | 20 + protocol/proto/LocalFilesSourceReport.proto | 12 + protocol/proto/MdnsLoginFailures.proto | 16 + protocol/proto/MercuryCacheReport.proto | 20 + .../MetadataExtensionClientStatistic.proto | 20 + protocol/proto/ModuleDebug.proto | 11 + protocol/proto/Offline2ClientError.proto | 13 + protocol/proto/Offline2ClientEvent.proto | 13 + protocol/proto/OfflineError.proto | 12 + protocol/proto/OfflineEvent.proto | 12 + protocol/proto/OfflineReport.proto | 26 + .../proto/OfflineUserPwdLoginNonAuth.proto | 11 + protocol/proto/PlaybackError.proto | 19 + protocol/proto/PlaybackRetry.proto | 15 + protocol/proto/PlaybackSegments.proto | 14 + protocol/proto/PlayerStateRestore.proto | 14 + protocol/proto/PlaylistSyncEvent.proto | 15 + protocol/proto/PodcastAdSegmentReceived.proto | 14 + protocol/proto/Prefetch.proto | 17 + protocol/proto/PrefetchError.proto | 12 + .../proto/ProductStateUcsVerification.proto | 13 + protocol/proto/PubSubCountPerIdent.proto | 13 + protocol/proto/ReachabilityChanged.proto | 12 + .../proto/RejectedClientEventNonAuth.proto | 12 + protocol/proto/RemainingSkips.proto | 14 + protocol/proto/RequestAccounting.proto | 18 + protocol/proto/RequestTime.proto | 22 + protocol/proto/StartTrack.proto | 13 + protocol/proto/Stutter.proto | 19 + protocol/proto/TierFeatureFlags.proto | 18 + protocol/proto/TrackNotPlayed.proto | 24 + protocol/proto/TrackStuck.proto | 18 + protocol/proto/WindowSize.proto | 14 + protocol/proto/ad-hermes-proxy.proto | 51 -- protocol/proto/anchor_extended_metadata.proto | 14 + protocol/proto/apiv1.proto | 113 +++++ protocol/proto/appstore.proto | 95 ---- protocol/proto/audio_files_extension.proto | 27 ++ protocol/proto/automix_mode.proto | 21 + protocol/proto/autoplay_context_request.proto | 12 + protocol/proto/autoplay_node.proto | 15 + protocol/proto/canvas.proto | 33 ++ protocol/proto/capping_data.proto | 30 ++ protocol/proto/claas.proto | 29 ++ protocol/proto/client_update.proto | 39 ++ protocol/proto/clips_cover.proto | 16 + protocol/proto/cloud_host_messages.proto | 152 ++++++ .../collection/album_collection_state.proto | 15 + .../collection/artist_collection_state.proto | 18 + .../collection/episode_collection_state.proto | 15 + .../collection/show_collection_state.proto | 13 + .../collection/track_collection_state.proto | 16 + protocol/proto/collection2v2.proto | 62 +++ protocol/proto/collection_index.proto | 40 ++ .../proto/collection_platform_requests.proto | 24 + .../proto/collection_platform_responses.proto | 19 + protocol/proto/collection_storage.proto | 20 + protocol/proto/composite_formats_node.proto | 31 ++ protocol/proto/concat_cosmos.proto | 22 + protocol/proto/connect.proto | 235 +++++++++ protocol/proto/connectivity.proto | 43 ++ protocol/proto/contains_request.proto | 17 + .../proto/content_access_token_cosmos.proto | 36 ++ protocol/proto/context.proto | 19 + protocol/proto/context_client_id.proto | 11 + protocol/proto/context_core.proto | 14 + protocol/proto/context_index.proto | 12 + protocol/proto/context_installation_id.proto | 11 + protocol/proto/context_monotonic_clock.proto | 12 + protocol/proto/context_node.proto | 23 + protocol/proto/context_page.proto | 17 + protocol/proto/context_player_ng.proto | 12 + protocol/proto/context_player_options.proto | 19 + protocol/proto/context_processor.proto | 19 + protocol/proto/context_sdk.proto | 11 + protocol/proto/context_time.proto | 11 + protocol/proto/context_track.proto | 14 + protocol/proto/context_view.proto | 18 + protocol/proto/context_view_cyclic_list.proto | 26 + protocol/proto/context_view_entry.proto | 25 + protocol/proto/context_view_entry_key.proto | 14 + .../core_configuration_applied_non_auth.proto | 11 + protocol/proto/cosmos_changes_request.proto | 11 + protocol/proto/cosmos_decorate_request.proto | 70 +++ .../proto/cosmos_get_album_list_request.proto | 37 ++ .../cosmos_get_artist_list_request.proto | 37 ++ .../cosmos_get_episode_list_request.proto | 27 ++ .../proto/cosmos_get_show_list_request.proto | 30 ++ .../proto/cosmos_get_tags_info_request.proto | 11 + ...smos_get_track_list_metadata_request.proto | 14 + .../proto/cosmos_get_track_list_request.proto | 39 ++ ...cosmos_get_unplayed_episodes_request.proto | 27 ++ protocol/proto/cuepoints.proto | 23 + protocol/proto/decorate_request.proto | 48 ++ .../proto/dependencies/session_control.proto | 121 +++++ protocol/proto/devices.proto | 35 ++ protocol/proto/display_segments.proto | 40 ++ protocol/proto/entity_extension_data.proto | 39 ++ protocol/proto/es_add_to_queue_request.proto | 19 + protocol/proto/es_command_options.proto | 15 + protocol/proto/es_context.proto | 21 + protocol/proto/es_context_page.proto | 19 + protocol/proto/es_context_player_error.proto | 55 +++ .../proto/es_context_player_options.proto | 23 + protocol/proto/es_context_player_state.proto | 82 ++++ protocol/proto/es_context_track.proto | 15 + protocol/proto/es_delete_session.proto | 17 + protocol/proto/es_get_error_request.proto | 13 + protocol/proto/es_get_play_history.proto | 19 + protocol/proto/es_get_position_state.proto | 25 + protocol/proto/es_get_queue_request.proto | 13 + protocol/proto/es_get_state_request.proto | 16 + protocol/proto/es_logging_params.proto | 18 + protocol/proto/es_optional.proto | 21 + protocol/proto/es_pause.proto | 17 + protocol/proto/es_play.proto | 28 ++ protocol/proto/es_play_options.proto | 32 ++ protocol/proto/es_play_origin.proto | 19 + protocol/proto/es_prepare_play.proto | 19 + protocol/proto/es_prepare_play_options.proto | 40 ++ protocol/proto/es_provided_track.proto | 18 + protocol/proto/es_queue.proto | 18 + protocol/proto/es_response_with_reasons.proto | 21 + protocol/proto/es_restrictions.proto | 33 ++ protocol/proto/es_resume.proto | 17 + protocol/proto/es_seek_to.proto | 18 + protocol/proto/es_session_response.proto | 13 + protocol/proto/es_set_options.proto | 21 + protocol/proto/es_set_queue_request.proto | 21 + protocol/proto/es_set_repeating_context.proto | 18 + protocol/proto/es_set_repeating_track.proto | 18 + protocol/proto/es_set_shuffling_context.proto | 18 + protocol/proto/es_skip_next.proto | 19 + protocol/proto/es_skip_prev.proto | 20 + protocol/proto/es_skip_to_track.proto | 19 + protocol/proto/es_stop.proto | 25 + protocol/proto/es_update.proto | 38 ++ protocol/proto/event_entity.proto | 18 + protocol/proto/explicit_content_pubsub.proto | 16 + protocol/proto/extended_metadata.proto | 62 +++ .../proto/extension_descriptor_type.proto | 29 ++ protocol/proto/extension_kind.proto | 46 ++ protocol/proto/extracted_colors.proto | 24 + protocol/proto/facebook-publish.proto | 51 -- protocol/proto/facebook.proto | 183 ------- protocol/proto/format.proto | 30 ++ protocol/proto/frecency.proto | 27 ++ protocol/proto/frecency_storage.proto | 25 + protocol/proto/gabito.proto | 36 ++ protocol/proto/global_node.proto | 16 + protocol/proto/google/protobuf/any.proto | 17 + .../proto/google/protobuf/descriptor.proto | 301 ++++++++++++ protocol/proto/google/protobuf/duration.proto | 18 + .../proto/google/protobuf/field_mask.proto | 17 + .../google/protobuf/source_context.proto | 16 + .../proto/google/protobuf/timestamp.proto | 18 + protocol/proto/google/protobuf/type.proto | 91 ++++ protocol/proto/google/protobuf/wrappers.proto | 49 ++ protocol/proto/identity.proto | 37 ++ protocol/proto/image-resolve.proto | 33 ++ protocol/proto/installation_data.proto | 18 + protocol/proto/instrumentation_params.proto | 12 + protocol/proto/lfs_secret_provider.proto | 11 + .../proto/liked_songs_tags_sync_state.proto | 12 + .../proto/listen_later_cosmos_response.proto | 28 ++ protocol/proto/local_bans_storage.proto | 17 + protocol/proto/local_sync_cosmos.proto | 16 + protocol/proto/local_sync_state.proto | 12 + protocol/proto/logging_params.proto | 14 + protocol/proto/mdata.proto | 43 ++ protocol/proto/mdata_cosmos.proto | 21 + protocol/proto/mdata_storage.proto | 38 ++ protocol/proto/media_manifest.proto | 55 +++ protocol/proto/media_type.proto | 14 + protocol/proto/media_type_node.proto | 13 + protocol/proto/mergedprofile.proto | 10 - protocol/proto/metadata.proto | 459 ++++++++++-------- protocol/proto/metadata/album_metadata.proto | 29 ++ protocol/proto/metadata/artist_metadata.proto | 18 + .../proto/metadata/episode_metadata.proto | 59 +++ protocol/proto/metadata/image_group.proto | 16 + protocol/proto/metadata/show_metadata.proto | 28 ++ protocol/proto/metadata/track_metadata.proto | 55 +++ protocol/proto/metadata_cosmos.proto | 30 ++ protocol/proto/modification_request.proto | 37 ++ protocol/proto/net-fortune.proto | 16 + protocol/proto/offline.proto | 83 ++++ .../proto/offline_playlists_containing.proto | 18 + protocol/proto/on_demand_in_free_reason.proto | 14 + .../proto/on_demand_set_cosmos_request.proto | 16 + .../proto/on_demand_set_cosmos_response.proto | 11 + protocol/proto/pin_request.proto | 33 ++ protocol/proto/play_origin.proto | 17 + protocol/proto/play_queue_node.proto | 19 + protocol/proto/play_reason.proto | 33 ++ protocol/proto/play_source.proto | 47 ++ protocol/proto/playback.proto | 17 + protocol/proto/playback_cosmos.proto | 105 ++++ protocol/proto/playback_segments.proto | 17 + protocol/proto/played_state.proto | 23 + .../played_state/episode_played_state.proto | 19 + .../playability_restriction.proto | 18 + .../played_state/show_played_state.proto | 13 + .../played_state/track_played_state.proto | 16 + protocol/proto/playedstate.proto | 40 ++ protocol/proto/player.proto | 211 ++++++++ protocol/proto/player_license.proto | 11 + protocol/proto/player_model.proto | 29 ++ protocol/proto/playlist4_external.proto | 239 +++++++++ protocol/proto/playlist_annotate3.proto | 41 ++ protocol/proto/playlist_folder_state.proto | 19 + protocol/proto/playlist_get_request.proto | 26 + .../proto/playlist_modification_request.proto | 22 + protocol/proto/playlist_permission.proto | 80 +++ protocol/proto/playlist_play_request.proto | 31 ++ .../proto/playlist_playback_request.proto | 13 + protocol/proto/playlist_playlist_state.proto | 50 ++ protocol/proto/playlist_query.proto | 63 +++ protocol/proto/playlist_request.proto | 89 ++++ ...playlist_set_base_permission_request.proto | 23 + .../playlist_set_permission_request.proto | 20 + protocol/proto/playlist_track_state.proto | 20 + protocol/proto/playlist_user_state.proto | 17 + protocol/proto/playlist_v1_uri.proto | 15 + protocol/proto/plugin.proto | 141 ++++++ protocol/proto/podcast_ad_segments.proto | 34 ++ protocol/proto/podcast_paywalls_cosmos.proto | 15 + protocol/proto/podcast_poll.proto | 48 ++ protocol/proto/podcast_qna.proto | 33 ++ protocol/proto/podcast_segments.proto | 47 ++ .../podcast_segments_cosmos_request.proto | 37 ++ .../podcast_segments_cosmos_response.proto | 39 ++ protocol/proto/podcast_subscription.proto | 22 + protocol/proto/podcast_virality.proto | 15 + protocol/proto/podcastextensions.proto | 29 ++ .../policy/album_decoration_policy.proto | 21 + .../policy/artist_decoration_policy.proto | 16 + .../policy/episode_decoration_policy.proto | 58 +++ .../policy/folder_decoration_policy.proto | 21 + .../playlist_album_decoration_policy.proto | 17 + .../policy/playlist_decoration_policy.proto | 60 +++ .../playlist_episode_decoration_policy.proto | 25 + .../playlist_request_decoration_policy.proto | 19 + .../playlist_track_decoration_policy.proto | 31 ++ .../rootlist_folder_decoration_policy.proto | 17 + .../rootlist_playlist_decoration_policy.proto | 17 + .../rootlist_request_decoration_policy.proto | 20 + .../proto/policy/show_decoration_policy.proto | 31 ++ .../policy/track_decoration_policy.proto | 36 ++ .../proto/policy/user_decoration_policy.proto | 31 ++ protocol/proto/popcount.proto | 13 - protocol/proto/popcount2_external.proto | 30 ++ protocol/proto/prepare_play_options.proto | 16 + protocol/proto/presence.proto | 94 ---- protocol/proto/profile_cache.proto | 19 + protocol/proto/profile_cosmos.proto | 22 + protocol/proto/property_definition.proto | 44 ++ protocol/proto/protobuf_delta.proto | 20 + protocol/proto/queue.proto | 14 + protocol/proto/radio.proto | 58 --- .../proto/rc_dummy_property_resolved.proto | 12 + protocol/proto/rcs.proto | 107 ++++ protocol/proto/recently_played.proto | 13 + protocol/proto/recently_played_backend.proto | 18 + protocol/proto/record_id.proto | 11 + protocol/proto/remote.proto | 19 + protocol/proto/repeating_track_node.proto | 15 + protocol/proto/request_failure.proto | 14 + protocol/proto/resolve.proto | 116 +++++ .../proto/resolve_configuration_error.proto | 14 + protocol/proto/resource_type.proto | 15 + protocol/proto/response_status.proto | 15 + protocol/proto/restrictions.proto | 31 ++ protocol/proto/resume_points_node.proto | 11 + protocol/proto/rootlist_request.proto | 43 ++ protocol/proto/search.proto | 44 -- protocol/proto/seek_to_position.proto | 12 + protocol/proto/sequence_number_entity.proto | 14 + protocol/proto/session.proto | 22 + protocol/proto/show_access.proto | 33 ++ protocol/proto/show_episode_state.proto | 25 + protocol/proto/show_request.proto | 66 +++ protocol/proto/show_show_state.proto | 15 + protocol/proto/skip_to_track.proto | 15 + protocol/proto/social.proto | 12 - protocol/proto/social_connect_v2.proto | 55 +++ protocol/proto/socialgraph.proto | 49 -- .../clienttoken/v0/clienttoken_http.proto | 123 +++++ .../spotify/login5/v3/challenges/code.proto | 26 + .../login5/v3/challenges/hashcash.proto | 22 + .../proto/spotify/login5/v3/client_info.proto | 15 + .../login5/v3/credentials/credentials.proto | 48 ++ .../login5/v3/identifiers/identifiers.proto | 16 + protocol/proto/spotify/login5/v3/login5.proto | 93 ++++ .../proto/spotify/login5/v3/user_info.proto | 29 ++ protocol/proto/status_code.proto | 12 + protocol/proto/status_response.proto | 15 + protocol/proto/storage-resolve.proto | 19 + protocol/proto/storage_cosmos.proto | 18 + protocol/proto/storylines.proto | 29 ++ protocol/proto/stream_end_request.proto | 18 + protocol/proto/stream_handle.proto | 12 + protocol/proto/stream_prepare_request.proto | 39 ++ protocol/proto/stream_prepare_response.proto | 18 + protocol/proto/stream_progress_request.proto | 22 + protocol/proto/stream_seek_request.proto | 14 + protocol/proto/stream_start_request.proto | 20 + protocol/proto/streaming_rule.proto | 17 + protocol/proto/suggest.proto | 43 -- protocol/proto/suppressions.proto | 11 + protocol/proto/sync/album_sync_state.proto | 15 + protocol/proto/sync/artist_sync_state.proto | 15 + protocol/proto/sync/episode_sync_state.proto | 14 + protocol/proto/sync/track_sync_state.proto | 14 + protocol/proto/sync_request.proto | 13 + .../proto/techu_core_exercise_cosmos.proto | 16 + protocol/proto/test_request_failure.proto | 14 + protocol/proto/toplist.proto | 6 - protocol/proto/track_instance.proto | 22 + protocol/proto/track_instantiator.proto | 13 + .../track_offlining_cosmos_response.proto | 24 + protocol/proto/transcripts.proto | 23 + protocol/proto/transfer_node.proto | 15 + protocol/proto/transfer_state.proto | 19 + protocol/proto/tts-resolve.proto | 49 ++ protocol/proto/ucs.proto | 56 +++ .../proto/unfinished_episodes_request.proto | 24 + protocol/proto/useraccount.proto | 15 + .../proto/your_library_contains_request.proto | 11 + .../your_library_contains_response.proto | 22 + .../proto/your_library_decorate_request.proto | 17 + .../your_library_decorate_response.proto | 19 + protocol/proto/your_library_entity.proto | 21 + protocol/proto/your_library_index.proto | 60 +++ protocol/proto/your_library_request.proto | 74 +++ protocol/proto/your_library_response.proto | 112 +++++ 396 files changed, 10952 insertions(+), 926 deletions(-) create mode 100644 protocol/proto/AdDecisionEvent.proto create mode 100644 protocol/proto/AdError.proto create mode 100644 protocol/proto/AdEvent.proto create mode 100644 protocol/proto/AdRequestEvent.proto create mode 100644 protocol/proto/AdSlotEvent.proto create mode 100644 protocol/proto/AmazonWakeUpTime.proto create mode 100644 protocol/proto/AudioDriverError.proto create mode 100644 protocol/proto/AudioDriverInfo.proto create mode 100644 protocol/proto/AudioFileSelection.proto create mode 100644 protocol/proto/AudioOffliningSettingsReport.proto create mode 100644 protocol/proto/AudioRateLimit.proto create mode 100644 protocol/proto/AudioSessionEvent.proto create mode 100644 protocol/proto/AudioSettingsReport.proto create mode 100644 protocol/proto/AudioStreamingSettingsReport.proto create mode 100644 protocol/proto/BoomboxPlaybackInstrumentation.proto create mode 100644 protocol/proto/BrokenObject.proto create mode 100644 protocol/proto/CacheError.proto create mode 100644 protocol/proto/CachePruningReport.proto create mode 100644 protocol/proto/CacheRealmPruningReport.proto create mode 100644 protocol/proto/CacheRealmReport.proto create mode 100644 protocol/proto/CacheReport.proto create mode 100644 protocol/proto/ClientLocale.proto create mode 100644 protocol/proto/ColdStartupSequence.proto create mode 100644 protocol/proto/CollectionLevelDbInfo.proto create mode 100644 protocol/proto/CollectionOfflineControllerEmptyTrackList.proto create mode 100644 protocol/proto/ConfigurationApplied.proto create mode 100644 protocol/proto/ConfigurationFetched.proto create mode 100644 protocol/proto/ConfigurationFetchedNonAuth.proto create mode 100644 protocol/proto/ConnectCredentialsRequest.proto create mode 100644 protocol/proto/ConnectDeviceDiscovered.proto create mode 100644 protocol/proto/ConnectDialError.proto create mode 100644 protocol/proto/ConnectMdnsPacketParseError.proto create mode 100644 protocol/proto/ConnectPullFailure.proto create mode 100644 protocol/proto/ConnectTransferResult.proto create mode 100644 protocol/proto/ConnectionError.proto create mode 100644 protocol/proto/ConnectionInfo.proto create mode 100644 protocol/proto/DefaultConfigurationApplied.proto create mode 100644 protocol/proto/DesktopAuthenticationFailureNonAuth.proto create mode 100644 protocol/proto/DesktopAuthenticationSuccess.proto create mode 100644 protocol/proto/DesktopGPUAccelerationInfo.proto create mode 100644 protocol/proto/DesktopHighMemoryUsage.proto create mode 100644 protocol/proto/DesktopUpdateDownloadComplete.proto create mode 100644 protocol/proto/DesktopUpdateDownloadError.proto create mode 100644 protocol/proto/DesktopUpdateMessageAction.proto create mode 100644 protocol/proto/DesktopUpdateMessageProcessed.proto create mode 100644 protocol/proto/DesktopUpdateResponse.proto create mode 100644 protocol/proto/Download.proto create mode 100644 protocol/proto/DrmRequestFailure.proto create mode 100644 protocol/proto/EndAd.proto create mode 100644 protocol/proto/EventSenderInternalErrorNonAuth.proto create mode 100644 protocol/proto/EventSenderStats.proto create mode 100644 protocol/proto/ExternalDeviceInfo.proto create mode 100644 protocol/proto/GetInfoFailures.proto create mode 100644 protocol/proto/HeadFileDownload.proto create mode 100644 protocol/proto/LocalFileSyncError.proto create mode 100644 protocol/proto/LocalFilesError.proto create mode 100644 protocol/proto/LocalFilesImport.proto create mode 100644 protocol/proto/LocalFilesReport.proto create mode 100644 protocol/proto/LocalFilesSourceReport.proto create mode 100644 protocol/proto/MdnsLoginFailures.proto create mode 100644 protocol/proto/MercuryCacheReport.proto create mode 100644 protocol/proto/MetadataExtensionClientStatistic.proto create mode 100644 protocol/proto/ModuleDebug.proto create mode 100644 protocol/proto/Offline2ClientError.proto create mode 100644 protocol/proto/Offline2ClientEvent.proto create mode 100644 protocol/proto/OfflineError.proto create mode 100644 protocol/proto/OfflineEvent.proto create mode 100644 protocol/proto/OfflineReport.proto create mode 100644 protocol/proto/OfflineUserPwdLoginNonAuth.proto create mode 100644 protocol/proto/PlaybackError.proto create mode 100644 protocol/proto/PlaybackRetry.proto create mode 100644 protocol/proto/PlaybackSegments.proto create mode 100644 protocol/proto/PlayerStateRestore.proto create mode 100644 protocol/proto/PlaylistSyncEvent.proto create mode 100644 protocol/proto/PodcastAdSegmentReceived.proto create mode 100644 protocol/proto/Prefetch.proto create mode 100644 protocol/proto/PrefetchError.proto create mode 100644 protocol/proto/ProductStateUcsVerification.proto create mode 100644 protocol/proto/PubSubCountPerIdent.proto create mode 100644 protocol/proto/ReachabilityChanged.proto create mode 100644 protocol/proto/RejectedClientEventNonAuth.proto create mode 100644 protocol/proto/RemainingSkips.proto create mode 100644 protocol/proto/RequestAccounting.proto create mode 100644 protocol/proto/RequestTime.proto create mode 100644 protocol/proto/StartTrack.proto create mode 100644 protocol/proto/Stutter.proto create mode 100644 protocol/proto/TierFeatureFlags.proto create mode 100644 protocol/proto/TrackNotPlayed.proto create mode 100644 protocol/proto/TrackStuck.proto create mode 100644 protocol/proto/WindowSize.proto delete mode 100644 protocol/proto/ad-hermes-proxy.proto create mode 100644 protocol/proto/anchor_extended_metadata.proto create mode 100644 protocol/proto/apiv1.proto delete mode 100644 protocol/proto/appstore.proto create mode 100644 protocol/proto/audio_files_extension.proto create mode 100644 protocol/proto/automix_mode.proto create mode 100644 protocol/proto/autoplay_context_request.proto create mode 100644 protocol/proto/autoplay_node.proto create mode 100644 protocol/proto/canvas.proto create mode 100644 protocol/proto/capping_data.proto create mode 100644 protocol/proto/claas.proto create mode 100644 protocol/proto/client_update.proto create mode 100644 protocol/proto/clips_cover.proto create mode 100644 protocol/proto/cloud_host_messages.proto create mode 100644 protocol/proto/collection/album_collection_state.proto create mode 100644 protocol/proto/collection/artist_collection_state.proto create mode 100644 protocol/proto/collection/episode_collection_state.proto create mode 100644 protocol/proto/collection/show_collection_state.proto create mode 100644 protocol/proto/collection/track_collection_state.proto create mode 100644 protocol/proto/collection2v2.proto create mode 100644 protocol/proto/collection_index.proto create mode 100644 protocol/proto/collection_platform_requests.proto create mode 100644 protocol/proto/collection_platform_responses.proto create mode 100644 protocol/proto/collection_storage.proto create mode 100644 protocol/proto/composite_formats_node.proto create mode 100644 protocol/proto/concat_cosmos.proto create mode 100644 protocol/proto/connect.proto create mode 100644 protocol/proto/connectivity.proto create mode 100644 protocol/proto/contains_request.proto create mode 100644 protocol/proto/content_access_token_cosmos.proto create mode 100644 protocol/proto/context.proto create mode 100644 protocol/proto/context_client_id.proto create mode 100644 protocol/proto/context_core.proto create mode 100644 protocol/proto/context_index.proto create mode 100644 protocol/proto/context_installation_id.proto create mode 100644 protocol/proto/context_monotonic_clock.proto create mode 100644 protocol/proto/context_node.proto create mode 100644 protocol/proto/context_page.proto create mode 100644 protocol/proto/context_player_ng.proto create mode 100644 protocol/proto/context_player_options.proto create mode 100644 protocol/proto/context_processor.proto create mode 100644 protocol/proto/context_sdk.proto create mode 100644 protocol/proto/context_time.proto create mode 100644 protocol/proto/context_track.proto create mode 100644 protocol/proto/context_view.proto create mode 100644 protocol/proto/context_view_cyclic_list.proto create mode 100644 protocol/proto/context_view_entry.proto create mode 100644 protocol/proto/context_view_entry_key.proto create mode 100644 protocol/proto/core_configuration_applied_non_auth.proto create mode 100644 protocol/proto/cosmos_changes_request.proto create mode 100644 protocol/proto/cosmos_decorate_request.proto create mode 100644 protocol/proto/cosmos_get_album_list_request.proto create mode 100644 protocol/proto/cosmos_get_artist_list_request.proto create mode 100644 protocol/proto/cosmos_get_episode_list_request.proto create mode 100644 protocol/proto/cosmos_get_show_list_request.proto create mode 100644 protocol/proto/cosmos_get_tags_info_request.proto create mode 100644 protocol/proto/cosmos_get_track_list_metadata_request.proto create mode 100644 protocol/proto/cosmos_get_track_list_request.proto create mode 100644 protocol/proto/cosmos_get_unplayed_episodes_request.proto create mode 100644 protocol/proto/cuepoints.proto create mode 100644 protocol/proto/decorate_request.proto create mode 100644 protocol/proto/dependencies/session_control.proto create mode 100644 protocol/proto/devices.proto create mode 100644 protocol/proto/display_segments.proto create mode 100644 protocol/proto/entity_extension_data.proto create mode 100644 protocol/proto/es_add_to_queue_request.proto create mode 100644 protocol/proto/es_command_options.proto create mode 100644 protocol/proto/es_context.proto create mode 100644 protocol/proto/es_context_page.proto create mode 100644 protocol/proto/es_context_player_error.proto create mode 100644 protocol/proto/es_context_player_options.proto create mode 100644 protocol/proto/es_context_player_state.proto create mode 100644 protocol/proto/es_context_track.proto create mode 100644 protocol/proto/es_delete_session.proto create mode 100644 protocol/proto/es_get_error_request.proto create mode 100644 protocol/proto/es_get_play_history.proto create mode 100644 protocol/proto/es_get_position_state.proto create mode 100644 protocol/proto/es_get_queue_request.proto create mode 100644 protocol/proto/es_get_state_request.proto create mode 100644 protocol/proto/es_logging_params.proto create mode 100644 protocol/proto/es_optional.proto create mode 100644 protocol/proto/es_pause.proto create mode 100644 protocol/proto/es_play.proto create mode 100644 protocol/proto/es_play_options.proto create mode 100644 protocol/proto/es_play_origin.proto create mode 100644 protocol/proto/es_prepare_play.proto create mode 100644 protocol/proto/es_prepare_play_options.proto create mode 100644 protocol/proto/es_provided_track.proto create mode 100644 protocol/proto/es_queue.proto create mode 100644 protocol/proto/es_response_with_reasons.proto create mode 100644 protocol/proto/es_restrictions.proto create mode 100644 protocol/proto/es_resume.proto create mode 100644 protocol/proto/es_seek_to.proto create mode 100644 protocol/proto/es_session_response.proto create mode 100644 protocol/proto/es_set_options.proto create mode 100644 protocol/proto/es_set_queue_request.proto create mode 100644 protocol/proto/es_set_repeating_context.proto create mode 100644 protocol/proto/es_set_repeating_track.proto create mode 100644 protocol/proto/es_set_shuffling_context.proto create mode 100644 protocol/proto/es_skip_next.proto create mode 100644 protocol/proto/es_skip_prev.proto create mode 100644 protocol/proto/es_skip_to_track.proto create mode 100644 protocol/proto/es_stop.proto create mode 100644 protocol/proto/es_update.proto create mode 100644 protocol/proto/event_entity.proto create mode 100644 protocol/proto/explicit_content_pubsub.proto create mode 100644 protocol/proto/extended_metadata.proto create mode 100644 protocol/proto/extension_descriptor_type.proto create mode 100644 protocol/proto/extension_kind.proto create mode 100644 protocol/proto/extracted_colors.proto delete mode 100644 protocol/proto/facebook-publish.proto delete mode 100644 protocol/proto/facebook.proto create mode 100644 protocol/proto/format.proto create mode 100644 protocol/proto/frecency.proto create mode 100644 protocol/proto/frecency_storage.proto create mode 100644 protocol/proto/gabito.proto create mode 100644 protocol/proto/global_node.proto create mode 100644 protocol/proto/google/protobuf/any.proto create mode 100644 protocol/proto/google/protobuf/descriptor.proto create mode 100644 protocol/proto/google/protobuf/duration.proto create mode 100644 protocol/proto/google/protobuf/field_mask.proto create mode 100644 protocol/proto/google/protobuf/source_context.proto create mode 100644 protocol/proto/google/protobuf/timestamp.proto create mode 100644 protocol/proto/google/protobuf/type.proto create mode 100644 protocol/proto/google/protobuf/wrappers.proto create mode 100644 protocol/proto/identity.proto create mode 100644 protocol/proto/image-resolve.proto create mode 100644 protocol/proto/installation_data.proto create mode 100644 protocol/proto/instrumentation_params.proto create mode 100644 protocol/proto/lfs_secret_provider.proto create mode 100644 protocol/proto/liked_songs_tags_sync_state.proto create mode 100644 protocol/proto/listen_later_cosmos_response.proto create mode 100644 protocol/proto/local_bans_storage.proto create mode 100644 protocol/proto/local_sync_cosmos.proto create mode 100644 protocol/proto/local_sync_state.proto create mode 100644 protocol/proto/logging_params.proto create mode 100644 protocol/proto/mdata.proto create mode 100644 protocol/proto/mdata_cosmos.proto create mode 100644 protocol/proto/mdata_storage.proto create mode 100644 protocol/proto/media_manifest.proto create mode 100644 protocol/proto/media_type.proto create mode 100644 protocol/proto/media_type_node.proto delete mode 100644 protocol/proto/mergedprofile.proto create mode 100644 protocol/proto/metadata/album_metadata.proto create mode 100644 protocol/proto/metadata/artist_metadata.proto create mode 100644 protocol/proto/metadata/episode_metadata.proto create mode 100644 protocol/proto/metadata/image_group.proto create mode 100644 protocol/proto/metadata/show_metadata.proto create mode 100644 protocol/proto/metadata/track_metadata.proto create mode 100644 protocol/proto/metadata_cosmos.proto create mode 100644 protocol/proto/modification_request.proto create mode 100644 protocol/proto/net-fortune.proto create mode 100644 protocol/proto/offline.proto create mode 100644 protocol/proto/offline_playlists_containing.proto create mode 100644 protocol/proto/on_demand_in_free_reason.proto create mode 100644 protocol/proto/on_demand_set_cosmos_request.proto create mode 100644 protocol/proto/on_demand_set_cosmos_response.proto create mode 100644 protocol/proto/pin_request.proto create mode 100644 protocol/proto/play_origin.proto create mode 100644 protocol/proto/play_queue_node.proto create mode 100644 protocol/proto/play_reason.proto create mode 100644 protocol/proto/play_source.proto create mode 100644 protocol/proto/playback.proto create mode 100644 protocol/proto/playback_cosmos.proto create mode 100644 protocol/proto/playback_segments.proto create mode 100644 protocol/proto/played_state.proto create mode 100644 protocol/proto/played_state/episode_played_state.proto create mode 100644 protocol/proto/played_state/playability_restriction.proto create mode 100644 protocol/proto/played_state/show_played_state.proto create mode 100644 protocol/proto/played_state/track_played_state.proto create mode 100644 protocol/proto/playedstate.proto create mode 100644 protocol/proto/player.proto create mode 100644 protocol/proto/player_license.proto create mode 100644 protocol/proto/player_model.proto create mode 100644 protocol/proto/playlist4_external.proto create mode 100644 protocol/proto/playlist_annotate3.proto create mode 100644 protocol/proto/playlist_folder_state.proto create mode 100644 protocol/proto/playlist_get_request.proto create mode 100644 protocol/proto/playlist_modification_request.proto create mode 100644 protocol/proto/playlist_permission.proto create mode 100644 protocol/proto/playlist_play_request.proto create mode 100644 protocol/proto/playlist_playback_request.proto create mode 100644 protocol/proto/playlist_playlist_state.proto create mode 100644 protocol/proto/playlist_query.proto create mode 100644 protocol/proto/playlist_request.proto create mode 100644 protocol/proto/playlist_set_base_permission_request.proto create mode 100644 protocol/proto/playlist_set_permission_request.proto create mode 100644 protocol/proto/playlist_track_state.proto create mode 100644 protocol/proto/playlist_user_state.proto create mode 100644 protocol/proto/playlist_v1_uri.proto create mode 100644 protocol/proto/plugin.proto create mode 100644 protocol/proto/podcast_ad_segments.proto create mode 100644 protocol/proto/podcast_paywalls_cosmos.proto create mode 100644 protocol/proto/podcast_poll.proto create mode 100644 protocol/proto/podcast_qna.proto create mode 100644 protocol/proto/podcast_segments.proto create mode 100644 protocol/proto/podcast_segments_cosmos_request.proto create mode 100644 protocol/proto/podcast_segments_cosmos_response.proto create mode 100644 protocol/proto/podcast_subscription.proto create mode 100644 protocol/proto/podcast_virality.proto create mode 100644 protocol/proto/podcastextensions.proto create mode 100644 protocol/proto/policy/album_decoration_policy.proto create mode 100644 protocol/proto/policy/artist_decoration_policy.proto create mode 100644 protocol/proto/policy/episode_decoration_policy.proto create mode 100644 protocol/proto/policy/folder_decoration_policy.proto create mode 100644 protocol/proto/policy/playlist_album_decoration_policy.proto create mode 100644 protocol/proto/policy/playlist_decoration_policy.proto create mode 100644 protocol/proto/policy/playlist_episode_decoration_policy.proto create mode 100644 protocol/proto/policy/playlist_request_decoration_policy.proto create mode 100644 protocol/proto/policy/playlist_track_decoration_policy.proto create mode 100644 protocol/proto/policy/rootlist_folder_decoration_policy.proto create mode 100644 protocol/proto/policy/rootlist_playlist_decoration_policy.proto create mode 100644 protocol/proto/policy/rootlist_request_decoration_policy.proto create mode 100644 protocol/proto/policy/show_decoration_policy.proto create mode 100644 protocol/proto/policy/track_decoration_policy.proto create mode 100644 protocol/proto/policy/user_decoration_policy.proto delete mode 100644 protocol/proto/popcount.proto create mode 100644 protocol/proto/popcount2_external.proto create mode 100644 protocol/proto/prepare_play_options.proto delete mode 100644 protocol/proto/presence.proto create mode 100644 protocol/proto/profile_cache.proto create mode 100644 protocol/proto/profile_cosmos.proto create mode 100644 protocol/proto/property_definition.proto create mode 100644 protocol/proto/protobuf_delta.proto create mode 100644 protocol/proto/queue.proto delete mode 100644 protocol/proto/radio.proto create mode 100644 protocol/proto/rc_dummy_property_resolved.proto create mode 100644 protocol/proto/rcs.proto create mode 100644 protocol/proto/recently_played.proto create mode 100644 protocol/proto/recently_played_backend.proto create mode 100644 protocol/proto/record_id.proto create mode 100644 protocol/proto/remote.proto create mode 100644 protocol/proto/repeating_track_node.proto create mode 100644 protocol/proto/request_failure.proto create mode 100644 protocol/proto/resolve.proto create mode 100644 protocol/proto/resolve_configuration_error.proto create mode 100644 protocol/proto/resource_type.proto create mode 100644 protocol/proto/response_status.proto create mode 100644 protocol/proto/restrictions.proto create mode 100644 protocol/proto/resume_points_node.proto create mode 100644 protocol/proto/rootlist_request.proto delete mode 100644 protocol/proto/search.proto create mode 100644 protocol/proto/seek_to_position.proto create mode 100644 protocol/proto/sequence_number_entity.proto create mode 100644 protocol/proto/session.proto create mode 100644 protocol/proto/show_access.proto create mode 100644 protocol/proto/show_episode_state.proto create mode 100644 protocol/proto/show_request.proto create mode 100644 protocol/proto/show_show_state.proto create mode 100644 protocol/proto/skip_to_track.proto delete mode 100644 protocol/proto/social.proto create mode 100644 protocol/proto/social_connect_v2.proto delete mode 100644 protocol/proto/socialgraph.proto create mode 100644 protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto create mode 100644 protocol/proto/spotify/login5/v3/challenges/code.proto create mode 100644 protocol/proto/spotify/login5/v3/challenges/hashcash.proto create mode 100644 protocol/proto/spotify/login5/v3/client_info.proto create mode 100644 protocol/proto/spotify/login5/v3/credentials/credentials.proto create mode 100644 protocol/proto/spotify/login5/v3/identifiers/identifiers.proto create mode 100644 protocol/proto/spotify/login5/v3/login5.proto create mode 100644 protocol/proto/spotify/login5/v3/user_info.proto create mode 100644 protocol/proto/status_code.proto create mode 100644 protocol/proto/status_response.proto create mode 100644 protocol/proto/storage-resolve.proto create mode 100644 protocol/proto/storage_cosmos.proto create mode 100644 protocol/proto/storylines.proto create mode 100644 protocol/proto/stream_end_request.proto create mode 100644 protocol/proto/stream_handle.proto create mode 100644 protocol/proto/stream_prepare_request.proto create mode 100644 protocol/proto/stream_prepare_response.proto create mode 100644 protocol/proto/stream_progress_request.proto create mode 100644 protocol/proto/stream_seek_request.proto create mode 100644 protocol/proto/stream_start_request.proto create mode 100644 protocol/proto/streaming_rule.proto delete mode 100644 protocol/proto/suggest.proto create mode 100644 protocol/proto/suppressions.proto create mode 100644 protocol/proto/sync/album_sync_state.proto create mode 100644 protocol/proto/sync/artist_sync_state.proto create mode 100644 protocol/proto/sync/episode_sync_state.proto create mode 100644 protocol/proto/sync/track_sync_state.proto create mode 100644 protocol/proto/sync_request.proto create mode 100644 protocol/proto/techu_core_exercise_cosmos.proto create mode 100644 protocol/proto/test_request_failure.proto delete mode 100644 protocol/proto/toplist.proto create mode 100644 protocol/proto/track_instance.proto create mode 100644 protocol/proto/track_instantiator.proto create mode 100644 protocol/proto/track_offlining_cosmos_response.proto create mode 100644 protocol/proto/transcripts.proto create mode 100644 protocol/proto/transfer_node.proto create mode 100644 protocol/proto/transfer_state.proto create mode 100644 protocol/proto/tts-resolve.proto create mode 100644 protocol/proto/ucs.proto create mode 100644 protocol/proto/unfinished_episodes_request.proto create mode 100644 protocol/proto/useraccount.proto create mode 100644 protocol/proto/your_library_contains_request.proto create mode 100644 protocol/proto/your_library_contains_response.proto create mode 100644 protocol/proto/your_library_decorate_request.proto create mode 100644 protocol/proto/your_library_decorate_response.proto create mode 100644 protocol/proto/your_library_entity.proto create mode 100644 protocol/proto/your_library_index.proto create mode 100644 protocol/proto/your_library_request.proto create mode 100644 protocol/proto/your_library_response.proto diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index d328a7d9..e7595f59 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -359,7 +359,7 @@ impl Metadata for Episode { let country = session.country(); let files = msg - .get_file() + .get_audio() .iter() .filter(|file| file.has_file_id()) .map(|file| { @@ -370,7 +370,7 @@ impl Metadata for Episode { .collect(); let covers = msg - .get_covers() + .get_cover_image() .get_image() .iter() .filter(|image| image.has_file_id()) @@ -412,7 +412,7 @@ impl Metadata for Show { .collect::>(); let covers = msg - .get_covers() + .get_cover_image() .get_image() .iter() .filter(|image| image.has_file_id()) diff --git a/playback/src/player.rs b/playback/src/player.rs index 8cbb4372..d67d2f88 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -652,12 +652,9 @@ impl PlayerTrackLoader { FileFormat::MP3_160 => 20 * 1024, FileFormat::MP3_96 => 12 * 1024, FileFormat::MP3_160_ENC => 20 * 1024, - FileFormat::MP4_128_DUAL => 16 * 1024, - FileFormat::OTHER3 => 40 * 1024, // better some high guess than nothing - FileFormat::AAC_160 => 20 * 1024, - FileFormat::AAC_320 => 40 * 1024, - FileFormat::MP4_128 => 16 * 1024, - FileFormat::OTHER5 => 40 * 1024, // better some high guess than nothing + FileFormat::AAC_24 => 3 * 1024, + FileFormat::AAC_48 => 6 * 1024, + FileFormat::FLAC_FLAC => 112 * 1024, // assume 900 kbps on average } } diff --git a/protocol/build.rs b/protocol/build.rs index c65c109a..53e04bc7 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -16,10 +16,11 @@ fn compile() { let proto_dir = Path::new(&env::var("CARGO_MANIFEST_DIR").expect("env")).join("proto"); let files = &[ + proto_dir.join("metadata.proto"), + // TODO: remove these legacy protobufs when we are on the new API completely proto_dir.join("authentication.proto"), proto_dir.join("keyexchange.proto"), proto_dir.join("mercury.proto"), - proto_dir.join("metadata.proto"), proto_dir.join("playlist4changes.proto"), proto_dir.join("playlist4content.proto"), proto_dir.join("playlist4issues.proto"), diff --git a/protocol/proto/AdDecisionEvent.proto b/protocol/proto/AdDecisionEvent.proto new file mode 100644 index 00000000..07a0a940 --- /dev/null +++ b/protocol/proto/AdDecisionEvent.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AdDecisionEvent { + optional string request_id = 1; + optional string decision_request_id = 2; + optional string decision_type = 3; +} diff --git a/protocol/proto/AdError.proto b/protocol/proto/AdError.proto new file mode 100644 index 00000000..1a69e788 --- /dev/null +++ b/protocol/proto/AdError.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AdError { + optional string request_type = 1; + optional string error_message = 2; + optional int64 http_error_code = 3; + optional string request_url = 4; + optional string tracking_event = 5; +} diff --git a/protocol/proto/AdEvent.proto b/protocol/proto/AdEvent.proto new file mode 100644 index 00000000..4b0a3059 --- /dev/null +++ b/protocol/proto/AdEvent.proto @@ -0,0 +1,27 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AdEvent { + optional string request_id = 1; + optional string app_startup_id = 2; + optional string ad_id = 3; + optional string lineitem_id = 4; + optional string creative_id = 5; + optional string slot = 6; + optional string format = 7; + optional string type = 8; + optional bool skippable = 9; + optional string event = 10; + optional string event_source = 11; + optional string event_reason = 12; + optional int32 event_sequence_num = 13; + optional int32 position = 14; + optional int32 duration = 15; + optional bool in_focus = 16; + optional float volume = 17; +} diff --git a/protocol/proto/AdRequestEvent.proto b/protocol/proto/AdRequestEvent.proto new file mode 100644 index 00000000..3ffdf863 --- /dev/null +++ b/protocol/proto/AdRequestEvent.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AdRequestEvent { + optional string feature_identifier = 1; + optional string requested_ad_type = 2; + optional int64 latency_ms = 3; + repeated string requested_ad_types = 4; +} diff --git a/protocol/proto/AdSlotEvent.proto b/protocol/proto/AdSlotEvent.proto new file mode 100644 index 00000000..1f345b69 --- /dev/null +++ b/protocol/proto/AdSlotEvent.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AdSlotEvent { + optional string event = 1; + optional string ad_id = 2; + optional string lineitem_id = 3; + optional string creative_id = 4; + optional string slot = 5; + optional string format = 6; + optional bool in_focus = 7; + optional string app_startup_id = 8; + optional string request_id = 9; +} diff --git a/protocol/proto/AmazonWakeUpTime.proto b/protocol/proto/AmazonWakeUpTime.proto new file mode 100644 index 00000000..25d64c48 --- /dev/null +++ b/protocol/proto/AmazonWakeUpTime.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AmazonWakeUpTime { + optional int64 delay_to_online = 1; +} diff --git a/protocol/proto/AudioDriverError.proto b/protocol/proto/AudioDriverError.proto new file mode 100644 index 00000000..3c97b461 --- /dev/null +++ b/protocol/proto/AudioDriverError.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AudioDriverError { + optional int64 error_code = 1; + optional string location = 2; + optional string driver_name = 3; + optional string additional_data = 4; +} diff --git a/protocol/proto/AudioDriverInfo.proto b/protocol/proto/AudioDriverInfo.proto new file mode 100644 index 00000000..23bae0a7 --- /dev/null +++ b/protocol/proto/AudioDriverInfo.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AudioDriverInfo { + optional string driver_name = 1; + optional string output_device_name = 2; + optional string output_device_category = 3; + optional string reason = 4; +} diff --git a/protocol/proto/AudioFileSelection.proto b/protocol/proto/AudioFileSelection.proto new file mode 100644 index 00000000..d99b36f4 --- /dev/null +++ b/protocol/proto/AudioFileSelection.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AudioFileSelection { + optional bytes playback_id = 1; + optional string strategy_name = 2; + optional int64 bitrate = 3; + optional bytes predict_id = 4; + optional string file_origin = 5; + optional int32 target_bitrate = 6; +} diff --git a/protocol/proto/AudioOffliningSettingsReport.proto b/protocol/proto/AudioOffliningSettingsReport.proto new file mode 100644 index 00000000..71d87f17 --- /dev/null +++ b/protocol/proto/AudioOffliningSettingsReport.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AudioOffliningSettingsReport { + optional string default_sync_bitrate_product_state = 1; + optional int64 user_selected_sync_bitrate = 2; + optional int64 sync_bitrate = 3; + optional bool sync_over_cellular = 4; + optional string primary_resource_type = 5; +} diff --git a/protocol/proto/AudioRateLimit.proto b/protocol/proto/AudioRateLimit.proto new file mode 100644 index 00000000..0ead830d --- /dev/null +++ b/protocol/proto/AudioRateLimit.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AudioRateLimit { + optional string driver_name = 1; + optional string output_device_name = 2; + optional string output_device_category = 3; + optional int64 max_size = 4; + optional int64 refill_per_milliseconds = 5; + optional int64 frames_requested = 6; + optional int64 frames_acquired = 7; + optional bytes playback_id = 8; +} diff --git a/protocol/proto/AudioSessionEvent.proto b/protocol/proto/AudioSessionEvent.proto new file mode 100644 index 00000000..c9b1a531 --- /dev/null +++ b/protocol/proto/AudioSessionEvent.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AudioSessionEvent { + optional string event = 1; + optional string context = 2; + optional string json_data = 3; +} diff --git a/protocol/proto/AudioSettingsReport.proto b/protocol/proto/AudioSettingsReport.proto new file mode 100644 index 00000000..e99ea8ec --- /dev/null +++ b/protocol/proto/AudioSettingsReport.proto @@ -0,0 +1,30 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AudioSettingsReport { + optional bool offline_mode = 1; + optional string default_play_bitrate_product_state = 2; + optional int64 user_selected_bitrate = 3; + optional int64 play_bitrate = 4; + optional bool low_bitrate_on_cellular = 5; + optional string default_sync_bitrate_product_state = 6; + optional int64 user_selected_sync_bitrate = 7; + optional int64 sync_bitrate = 8; + optional bool sync_over_cellular = 9; + optional string enable_gapless_product_state = 10; + optional bool enable_gapless = 11; + optional string enable_crossfade_product_state = 12; + optional bool enable_crossfade = 13; + optional int64 crossfade_time = 14; + optional bool enable_normalization = 15; + optional int64 playback_speed = 16; + optional string audio_loudness_level = 17; + optional bool enable_automix = 18; + optional bool enable_silence_trimmer = 19; + optional bool enable_mono_downmixer = 20; +} diff --git a/protocol/proto/AudioStreamingSettingsReport.proto b/protocol/proto/AudioStreamingSettingsReport.proto new file mode 100644 index 00000000..ef6e4730 --- /dev/null +++ b/protocol/proto/AudioStreamingSettingsReport.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message AudioStreamingSettingsReport { + optional string default_play_bitrate_product_state = 1; + optional int64 user_selected_play_bitrate_cellular = 2; + optional int64 user_selected_play_bitrate_wifi = 3; + optional int64 play_bitrate_cellular = 4; + optional int64 play_bitrate_wifi = 5; + optional bool allow_downgrade = 6; +} diff --git a/protocol/proto/BoomboxPlaybackInstrumentation.proto b/protocol/proto/BoomboxPlaybackInstrumentation.proto new file mode 100644 index 00000000..01e3f2c7 --- /dev/null +++ b/protocol/proto/BoomboxPlaybackInstrumentation.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message BoomboxPlaybackInstrumentation { + optional bytes playback_id = 1; + optional bool was_playback_paused = 2; + repeated string dimensions = 3; + map total_buffer_size = 4; + map number_of_calls = 5; + map total_duration = 6; + map first_call_time = 7; + map last_call_time = 8; +} diff --git a/protocol/proto/BrokenObject.proto b/protocol/proto/BrokenObject.proto new file mode 100644 index 00000000..3bdb6677 --- /dev/null +++ b/protocol/proto/BrokenObject.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message BrokenObject { + optional string type = 1; + optional string id = 2; + optional int64 error_code = 3; + optional bytes playback_id = 4; +} diff --git a/protocol/proto/CacheError.proto b/protocol/proto/CacheError.proto new file mode 100644 index 00000000..8da6196d --- /dev/null +++ b/protocol/proto/CacheError.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message CacheError { + optional int64 error_code = 1; + optional int64 os_error_code = 2; + optional string realm = 3; + optional bytes file_id = 4; + optional int64 num_errors = 5; + optional string cache_path = 6; +} diff --git a/protocol/proto/CachePruningReport.proto b/protocol/proto/CachePruningReport.proto new file mode 100644 index 00000000..3225f1d5 --- /dev/null +++ b/protocol/proto/CachePruningReport.proto @@ -0,0 +1,25 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message CachePruningReport { + optional bytes cache_id = 1; + optional int64 time_spent_pruning_ms = 2; + optional int64 size_before_prune_kb = 3; + optional int64 size_after_prune_kb = 4; + optional int64 num_entries_pruned = 5; + optional int64 num_entries_pruned_expired = 6; + optional int64 size_entries_pruned_expired_kb = 7; + optional int64 num_entries_pruned_limit = 8; + optional int64 size_pruned_limit_kb = 9; + optional int64 num_entries_pruned_never_used = 10; + optional int64 size_pruned_never_used_kb = 11; + optional int64 num_entries_pruned_max_realm_size = 12; + optional int64 size_pruned_max_realm_size_kb = 13; + optional int64 num_entries_pruned_min_free_space = 14; + optional int64 size_pruned_min_free_space_kb = 15; +} diff --git a/protocol/proto/CacheRealmPruningReport.proto b/protocol/proto/CacheRealmPruningReport.proto new file mode 100644 index 00000000..479a26a5 --- /dev/null +++ b/protocol/proto/CacheRealmPruningReport.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message CacheRealmPruningReport { + optional bytes cache_id = 1; + optional int64 realm_id = 2; + optional int64 num_entries_pruned = 3; + optional int64 num_entries_pruned_expired = 4; + optional int64 size_entries_pruned_expired_kb = 5; + optional int64 num_entries_pruned_limit = 6; + optional int64 size_pruned_limit_kb = 7; + optional int64 num_entries_pruned_never_used = 8; + optional int64 size_pruned_never_used_kb = 9; + optional int64 num_entries_pruned_max_realm_size = 10; + optional int64 size_pruned_max_realm_size_kb = 11; + optional int64 num_entries_pruned_min_free_space = 12; + optional int64 size_pruned_min_free_space_kb = 13; +} diff --git a/protocol/proto/CacheRealmReport.proto b/protocol/proto/CacheRealmReport.proto new file mode 100644 index 00000000..4d3c8a55 --- /dev/null +++ b/protocol/proto/CacheRealmReport.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message CacheRealmReport { + optional bytes cache_id = 1; + optional int64 realm_id = 2; + optional int64 num_entries = 3; + optional int64 num_locked_entries = 4; + optional int64 num_locked_entries_current_user = 5; + optional int64 num_full_entries = 6; + optional int64 size_kb = 7; + optional int64 locked_size_kb = 8; +} diff --git a/protocol/proto/CacheReport.proto b/protocol/proto/CacheReport.proto new file mode 100644 index 00000000..c8666ca3 --- /dev/null +++ b/protocol/proto/CacheReport.proto @@ -0,0 +1,30 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message CacheReport { + optional bytes cache_id = 1; + optional int64 max_cache_size = 2; + optional int64 free_space = 3; + optional int64 total_space = 4; + optional int64 cache_age = 5; + optional int64 num_users_with_locked_entries = 6; + optional int64 permanent_files = 7; + optional int64 permanent_size_kb = 8; + optional int64 unknown_permanent_files = 9; + optional int64 unknown_permanent_size_kb = 10; + optional int64 volatile_files = 11; + optional int64 volatile_size_kb = 12; + optional int64 unknown_volatile_files = 13; + optional int64 unknown_volatile_size_kb = 14; + optional int64 num_entries = 15; + optional int64 num_locked_entries = 16; + optional int64 num_locked_entries_current_user = 17; + optional int64 num_full_entries = 18; + optional int64 size_kb = 19; + optional int64 locked_size_kb = 20; +} diff --git a/protocol/proto/ClientLocale.proto b/protocol/proto/ClientLocale.proto new file mode 100644 index 00000000..a8e330b3 --- /dev/null +++ b/protocol/proto/ClientLocale.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ClientLocale { + optional string client_default_locale = 1; + optional string user_specified_locale = 2; +} diff --git a/protocol/proto/ColdStartupSequence.proto b/protocol/proto/ColdStartupSequence.proto new file mode 100644 index 00000000..cfeedee9 --- /dev/null +++ b/protocol/proto/ColdStartupSequence.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ColdStartupSequence { + optional string terminal_state = 1; + map steps = 2; + map metadata = 3; + optional string connection_type = 4; + optional string initial_application_state = 5; + optional string terminal_application_state = 6; + optional string view_load_sequence_id = 7; + optional int32 device_year_class = 8; + map subdurations = 9; +} diff --git a/protocol/proto/CollectionLevelDbInfo.proto b/protocol/proto/CollectionLevelDbInfo.proto new file mode 100644 index 00000000..4f222487 --- /dev/null +++ b/protocol/proto/CollectionLevelDbInfo.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message CollectionLevelDbInfo { + optional string bucket = 1; + optional bool use_leveldb = 2; + optional bool migration_from_file_ok = 3; + optional bool index_check_ok = 4; + optional bool leveldb_works = 5; + optional bool already_migrated = 6; +} diff --git a/protocol/proto/CollectionOfflineControllerEmptyTrackList.proto b/protocol/proto/CollectionOfflineControllerEmptyTrackList.proto new file mode 100644 index 00000000..ee830433 --- /dev/null +++ b/protocol/proto/CollectionOfflineControllerEmptyTrackList.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message CollectionOfflineControllerEmptyTrackList { + optional string link_type = 1; + optional bool consistent_with_collection = 2; + optional int64 collection_size = 3; +} diff --git a/protocol/proto/ConfigurationApplied.proto b/protocol/proto/ConfigurationApplied.proto new file mode 100644 index 00000000..40aad33c --- /dev/null +++ b/protocol/proto/ConfigurationApplied.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConfigurationApplied { + optional int64 last_rcs_fetch_time = 1; + optional string installation_id = 2; + repeated int32 policy_group_ids = 3; + optional string configuration_assignment_id = 4; + optional string rc_client_id = 5; + optional string rc_client_version = 6; + optional string platform = 7; + optional string fetch_type = 8; +} diff --git a/protocol/proto/ConfigurationFetched.proto b/protocol/proto/ConfigurationFetched.proto new file mode 100644 index 00000000..bb61a2e0 --- /dev/null +++ b/protocol/proto/ConfigurationFetched.proto @@ -0,0 +1,31 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConfigurationFetched { + optional int64 last_rcs_fetch_time = 1; + optional string installation_id = 2; + optional string configuration_assignment_id = 3; + optional string property_set_id = 4; + optional string attributes_set_id = 5; + optional string rc_client_id = 6; + optional string rc_client_version = 7; + optional string rc_sdk_version = 8; + optional string platform = 9; + optional string fetch_type = 10; + optional int64 latency = 11; + optional int64 payload_size = 12; + optional int32 status_code = 13; + optional string error_reason = 14; + optional string error_message = 15; + optional string error_reason_configuration_resolve = 16; + optional string error_message_configuration_resolve = 17; + optional string error_reason_account_attributes = 18; + optional string error_message_account_attributes = 19; + optional int32 error_code_account_attributes = 20; + optional int32 error_code_configuration_resolve = 21; +} diff --git a/protocol/proto/ConfigurationFetchedNonAuth.proto b/protocol/proto/ConfigurationFetchedNonAuth.proto new file mode 100644 index 00000000..e28d1d39 --- /dev/null +++ b/protocol/proto/ConfigurationFetchedNonAuth.proto @@ -0,0 +1,31 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConfigurationFetchedNonAuth { + optional int64 last_rcs_fetch_time = 1; + optional string installation_id = 2; + optional string configuration_assignment_id = 3; + optional string property_set_id = 4; + optional string attributes_set_id = 5; + optional string rc_client_id = 6; + optional string rc_client_version = 7; + optional string rc_sdk_version = 8; + optional string platform = 9; + optional string fetch_type = 10; + optional int64 latency = 11; + optional int64 payload_size = 12; + optional int32 status_code = 13; + optional string error_reason = 14; + optional string error_message = 15; + optional string error_reason_configuration_resolve = 16; + optional string error_message_configuration_resolve = 17; + optional string error_reason_account_attributes = 18; + optional string error_message_account_attributes = 19; + optional int32 error_code_account_attributes = 20; + optional int32 error_code_configuration_resolve = 21; +} diff --git a/protocol/proto/ConnectCredentialsRequest.proto b/protocol/proto/ConnectCredentialsRequest.proto new file mode 100644 index 00000000..d3e91cf3 --- /dev/null +++ b/protocol/proto/ConnectCredentialsRequest.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConnectCredentialsRequest { + optional string token_type = 1; + optional string client_id = 2; +} diff --git a/protocol/proto/ConnectDeviceDiscovered.proto b/protocol/proto/ConnectDeviceDiscovered.proto new file mode 100644 index 00000000..bb156ff7 --- /dev/null +++ b/protocol/proto/ConnectDeviceDiscovered.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConnectDeviceDiscovered { + optional string device_id = 1; + optional string discover_method = 2; + optional string discovered_device_id = 3; + optional string discovered_device_type = 4; + optional string discovered_library_version = 5; + optional string discovered_brand_display_name = 6; + optional string discovered_model_display_name = 7; + optional string discovered_client_id = 8; + optional string discovered_product_id = 9; + optional string discovered_device_availablilty = 10; + optional string discovered_device_public_key = 11; + optional bool capabilities_resolved = 12; +} diff --git a/protocol/proto/ConnectDialError.proto b/protocol/proto/ConnectDialError.proto new file mode 100644 index 00000000..90a8f36a --- /dev/null +++ b/protocol/proto/ConnectDialError.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConnectDialError { + optional string type = 1; + optional string request = 2; + optional string response = 3; + optional int64 error = 4; + optional string context = 5; +} diff --git a/protocol/proto/ConnectMdnsPacketParseError.proto b/protocol/proto/ConnectMdnsPacketParseError.proto new file mode 100644 index 00000000..e7685828 --- /dev/null +++ b/protocol/proto/ConnectMdnsPacketParseError.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConnectMdnsPacketParseError { + optional string type = 1; + optional string buffer = 2; + optional string ttl = 3; + optional string txt = 4; + optional string host = 5; + optional string discovery_name = 6; + optional string context = 7; +} diff --git a/protocol/proto/ConnectPullFailure.proto b/protocol/proto/ConnectPullFailure.proto new file mode 100644 index 00000000..fc1f9819 --- /dev/null +++ b/protocol/proto/ConnectPullFailure.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConnectPullFailure { + optional bytes transfer_data = 1; + optional int64 error_code = 2; + map reasons = 3; +} diff --git a/protocol/proto/ConnectTransferResult.proto b/protocol/proto/ConnectTransferResult.proto new file mode 100644 index 00000000..9239e845 --- /dev/null +++ b/protocol/proto/ConnectTransferResult.proto @@ -0,0 +1,29 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConnectTransferResult { + optional string result = 1; + optional string device_type = 2; + optional string discovery_class = 3; + optional string device_model = 4; + optional string device_brand = 5; + optional string device_software_version = 6; + optional int64 duration = 7; + optional string device_client_id = 8; + optional string transfer_intent_id = 9; + optional string transfer_debug_log = 10; + optional string error_code = 11; + optional int32 http_response_code = 12; + optional string initial_device_state = 13; + optional int32 retry_count = 14; + optional int32 login_retry_count = 15; + optional int64 login_duration = 16; + optional string target_device_id = 17; + optional bool target_device_is_local = 18; + optional string final_device_state = 19; +} diff --git a/protocol/proto/ConnectionError.proto b/protocol/proto/ConnectionError.proto new file mode 100644 index 00000000..8c1c35bd --- /dev/null +++ b/protocol/proto/ConnectionError.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConnectionError { + optional int64 error_code = 1; + optional string ap = 2; + optional string proxy = 3; +} diff --git a/protocol/proto/ConnectionInfo.proto b/protocol/proto/ConnectionInfo.proto new file mode 100644 index 00000000..2c830ed5 --- /dev/null +++ b/protocol/proto/ConnectionInfo.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ConnectionInfo { + optional string ap = 1; + optional string proxy = 2; + optional bool user_initated_login = 3; + optional string reachability_type = 4; + optional string web_installer_unique_id = 5; + optional string ap_resolve_source = 6; + optional string address_type = 7; + optional bool ipv6_failed = 8; +} diff --git a/protocol/proto/DefaultConfigurationApplied.proto b/protocol/proto/DefaultConfigurationApplied.proto new file mode 100644 index 00000000..9236ecb9 --- /dev/null +++ b/protocol/proto/DefaultConfigurationApplied.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DefaultConfigurationApplied { + optional string installation_id = 1; + optional string configuration_assignment_id = 2; + optional string rc_client_id = 3; + optional string rc_client_version = 4; + optional string platform = 5; + optional string fetch_type = 6; + optional string reason = 7; +} diff --git a/protocol/proto/DesktopAuthenticationFailureNonAuth.proto b/protocol/proto/DesktopAuthenticationFailureNonAuth.proto new file mode 100644 index 00000000..e3b495ec --- /dev/null +++ b/protocol/proto/DesktopAuthenticationFailureNonAuth.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopAuthenticationFailureNonAuth { + optional string action_hash = 1; + optional string error_category = 2; + optional int32 error_code = 3; +} diff --git a/protocol/proto/DesktopAuthenticationSuccess.proto b/protocol/proto/DesktopAuthenticationSuccess.proto new file mode 100644 index 00000000..8814df79 --- /dev/null +++ b/protocol/proto/DesktopAuthenticationSuccess.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopAuthenticationSuccess { + optional string action_hash = 1; +} diff --git a/protocol/proto/DesktopGPUAccelerationInfo.proto b/protocol/proto/DesktopGPUAccelerationInfo.proto new file mode 100644 index 00000000..2fbaed08 --- /dev/null +++ b/protocol/proto/DesktopGPUAccelerationInfo.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopGPUAccelerationInfo { + optional bool is_enabled = 1; +} diff --git a/protocol/proto/DesktopHighMemoryUsage.proto b/protocol/proto/DesktopHighMemoryUsage.proto new file mode 100644 index 00000000..e55106e3 --- /dev/null +++ b/protocol/proto/DesktopHighMemoryUsage.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopHighMemoryUsage { + optional bool is_continuation_event = 1; + optional double sample_time_interval_seconds = 2; + optional int64 win_committed_bytes = 3; + optional int64 win_peak_committed_bytes = 4; + optional int64 win_working_set_bytes = 5; + optional int64 win_peak_working_set_bytes = 6; + optional int64 mac_virtual_size_bytes = 7; + optional int64 mac_resident_size_bytes = 8; + optional int64 mac_footprint_bytes = 9; +} diff --git a/protocol/proto/DesktopUpdateDownloadComplete.proto b/protocol/proto/DesktopUpdateDownloadComplete.proto new file mode 100644 index 00000000..bf1fe4d9 --- /dev/null +++ b/protocol/proto/DesktopUpdateDownloadComplete.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopUpdateDownloadComplete { + optional int64 revision = 1; + optional bool is_critical = 2; + optional string source = 3; + optional bool is_successful = 4; + optional bool is_employee = 5; +} diff --git a/protocol/proto/DesktopUpdateDownloadError.proto b/protocol/proto/DesktopUpdateDownloadError.proto new file mode 100644 index 00000000..8385d4a1 --- /dev/null +++ b/protocol/proto/DesktopUpdateDownloadError.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopUpdateDownloadError { + optional int64 revision = 1; + optional bool is_critical = 2; + optional string error_message = 3; + optional string source = 4; + optional bool is_employee = 5; +} diff --git a/protocol/proto/DesktopUpdateMessageAction.proto b/protocol/proto/DesktopUpdateMessageAction.proto new file mode 100644 index 00000000..3ff5efea --- /dev/null +++ b/protocol/proto/DesktopUpdateMessageAction.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopUpdateMessageAction { + optional bool will_download = 1; + optional int64 this_message_from_revision = 2; + optional int64 this_message_to_revision = 3; + optional bool is_critical = 4; + optional int64 already_downloaded_from_revision = 5; + optional int64 already_downloaded_to_revision = 6; + optional string source = 7; + optional bool is_employee = 8; +} diff --git a/protocol/proto/DesktopUpdateMessageProcessed.proto b/protocol/proto/DesktopUpdateMessageProcessed.proto new file mode 100644 index 00000000..71b2e766 --- /dev/null +++ b/protocol/proto/DesktopUpdateMessageProcessed.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopUpdateMessageProcessed { + optional bool success = 1; + optional string source = 2; + optional int64 revision = 3; + optional bool is_critical = 4; + optional string binary_hash = 5; + optional bool is_employee = 6; +} diff --git a/protocol/proto/DesktopUpdateResponse.proto b/protocol/proto/DesktopUpdateResponse.proto new file mode 100644 index 00000000..683672f2 --- /dev/null +++ b/protocol/proto/DesktopUpdateResponse.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DesktopUpdateResponse { + optional int64 status_code = 1; + optional int64 request_time_ms = 2; + optional int64 payload_size = 3; + optional bool is_employee = 4; + optional string error_message = 5; +} diff --git a/protocol/proto/Download.proto b/protocol/proto/Download.proto new file mode 100644 index 00000000..417236bd --- /dev/null +++ b/protocol/proto/Download.proto @@ -0,0 +1,53 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message Download { + optional bytes file_id = 1; + optional bytes playback_id = 2; + optional int64 bytes_from_ap = 3; + optional int64 waste_from_ap = 4; + optional int64 reqs_from_ap = 5; + optional int64 error_from_ap = 6; + optional int64 bytes_from_cdn = 7; + optional int64 waste_from_cdn = 8; + optional int64 bytes_from_cache = 9; + optional int64 content_size = 10; + optional string content_type = 11; + optional int64 ap_initial_latency = 12; + optional int64 ap_max_latency = 13; + optional int64 ap_min_latency = 14; + optional double ap_avg_latency = 15; + optional int64 ap_median_latency = 16; + optional double ap_avg_bw = 17; + optional int64 cdn_initial_latency = 18; + optional int64 cdn_max_latency = 19; + optional int64 cdn_min_latency = 20; + optional double cdn_avg_latency = 21; + optional int64 cdn_median_latency = 22; + optional int64 cdn_64k_initial_latency = 23; + optional int64 cdn_64k_max_latency = 24; + optional int64 cdn_64k_min_latency = 25; + optional double cdn_64k_avg_latency = 26; + optional int64 cdn_64k_median_latency = 27; + optional double cdn_avg_bw = 28; + optional double cdn_initial_bw_estimate = 29; + optional string cdn_uri_scheme = 30; + optional string cdn_domain = 31; + optional string cdn_socket_reuse = 32; + optional int64 num_cache_error = 33; + optional int64 bytes_from_carrier = 34; + optional int64 bytes_from_unknown = 35; + optional int64 bytes_from_wifi = 36; + optional int64 bytes_from_ethernet = 37; + optional string request_type = 38; + optional int64 total_time = 39; + optional int64 bitrate = 40; + optional int64 reqs_from_cdn = 41; + optional int64 error_from_cdn = 42; + optional string file_origin = 43; +} diff --git a/protocol/proto/DrmRequestFailure.proto b/protocol/proto/DrmRequestFailure.proto new file mode 100644 index 00000000..8f7df231 --- /dev/null +++ b/protocol/proto/DrmRequestFailure.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message DrmRequestFailure { + optional string reason = 1; + optional int64 error_code = 2; + optional bool fatal = 3; + optional bytes playback_id = 4; +} diff --git a/protocol/proto/EndAd.proto b/protocol/proto/EndAd.proto new file mode 100644 index 00000000..cff0b7b6 --- /dev/null +++ b/protocol/proto/EndAd.proto @@ -0,0 +1,34 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message EndAd { + optional bytes file_id = 1; + optional bytes playback_id = 2; + optional bytes song_id = 3; + optional string source_start = 4; + optional string reason_start = 5; + optional string source_end = 6; + optional string reason_end = 7; + optional int64 bytes_played = 8; + optional int64 bytes_in_song = 9; + optional int64 ms_played = 10; + optional int64 ms_total_est = 11; + optional int64 ms_rcv_latency = 12; + optional int64 n_seekback = 13; + optional int64 ms_seekback = 14; + optional int64 n_seekfwd = 15; + optional int64 ms_seekfwd = 16; + optional int64 ms_latency = 17; + optional int64 n_stutter = 18; + optional int64 p_lowbuffer = 19; + optional bool skipped = 20; + optional bool ad_clicked = 21; + optional string token = 22; + optional int64 client_ad_count = 23; + optional int64 client_campaign_count = 24; +} diff --git a/protocol/proto/EventSenderInternalErrorNonAuth.proto b/protocol/proto/EventSenderInternalErrorNonAuth.proto new file mode 100644 index 00000000..e6fe182a --- /dev/null +++ b/protocol/proto/EventSenderInternalErrorNonAuth.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message EventSenderInternalErrorNonAuth { + optional string error_message = 1; + optional string error_type = 2; + optional string error_context = 3; + optional int32 error_code = 4; +} diff --git a/protocol/proto/EventSenderStats.proto b/protocol/proto/EventSenderStats.proto new file mode 100644 index 00000000..88be6fe1 --- /dev/null +++ b/protocol/proto/EventSenderStats.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message EventSenderStats { + map storage_size = 1; + map sequence_number_min = 2; + map sequence_number_next = 3; +} diff --git a/protocol/proto/ExternalDeviceInfo.proto b/protocol/proto/ExternalDeviceInfo.proto new file mode 100644 index 00000000..f590df22 --- /dev/null +++ b/protocol/proto/ExternalDeviceInfo.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ExternalDeviceInfo { + optional string type = 1; + optional string subtype = 2; + optional string reason = 3; + optional bool taken_over = 4; + optional int64 num_tracks = 5; + optional int64 num_purchased_tracks = 6; + optional int64 num_playlists = 7; + optional string error = 8; + optional bool full = 9; + optional bool sync_all = 10; +} diff --git a/protocol/proto/GetInfoFailures.proto b/protocol/proto/GetInfoFailures.proto new file mode 100644 index 00000000..868ae5b7 --- /dev/null +++ b/protocol/proto/GetInfoFailures.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message GetInfoFailures { + optional string device_id = 1; + optional int64 error_code = 2; + optional string request = 3; + optional string response_body = 4; + optional string context = 5; +} diff --git a/protocol/proto/HeadFileDownload.proto b/protocol/proto/HeadFileDownload.proto new file mode 100644 index 00000000..acfa87fa --- /dev/null +++ b/protocol/proto/HeadFileDownload.proto @@ -0,0 +1,26 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message HeadFileDownload { + optional bytes file_id = 1; + optional bytes playback_id = 2; + optional string cdn_uri_scheme = 3; + optional string cdn_domain = 4; + optional int64 head_file_size = 5; + optional int64 bytes_downloaded = 6; + optional int64 bytes_wasted = 7; + optional int64 http_latency = 8; + optional int64 http_64k_latency = 9; + optional int64 total_time = 10; + optional int64 http_result = 11; + optional int64 error_code = 12; + optional int64 cached_bytes = 13; + optional int64 bytes_from_cache = 14; + optional string socket_reuse = 15; + optional string request_type = 16; +} diff --git a/protocol/proto/LocalFileSyncError.proto b/protocol/proto/LocalFileSyncError.proto new file mode 100644 index 00000000..0403dba1 --- /dev/null +++ b/protocol/proto/LocalFileSyncError.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message LocalFileSyncError { + optional string error = 1; +} diff --git a/protocol/proto/LocalFilesError.proto b/protocol/proto/LocalFilesError.proto new file mode 100644 index 00000000..49347341 --- /dev/null +++ b/protocol/proto/LocalFilesError.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message LocalFilesError { + optional int64 error_code = 1; + optional string context = 2; +} diff --git a/protocol/proto/LocalFilesImport.proto b/protocol/proto/LocalFilesImport.proto new file mode 100644 index 00000000..4deff70f --- /dev/null +++ b/protocol/proto/LocalFilesImport.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message LocalFilesImport { + optional int64 tracks = 1; + optional int64 duplicate_tracks = 2; + optional int64 failed_tracks = 3; + optional int64 matched_tracks = 4; + optional string source = 5; +} diff --git a/protocol/proto/LocalFilesReport.proto b/protocol/proto/LocalFilesReport.proto new file mode 100644 index 00000000..cd5c99d7 --- /dev/null +++ b/protocol/proto/LocalFilesReport.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message LocalFilesReport { + optional int64 total_tracks = 1; + optional int64 total_size = 2; + optional int64 owned_tracks = 3; + optional int64 owned_size = 4; + optional int64 tracks_not_found = 5; + optional int64 tracks_bad_format = 6; + optional int64 tracks_drm_protected = 7; + optional int64 tracks_unknown_pruned = 8; + optional int64 tracks_reallocated_repaired = 9; + optional int64 enabled_sources = 10; +} diff --git a/protocol/proto/LocalFilesSourceReport.proto b/protocol/proto/LocalFilesSourceReport.proto new file mode 100644 index 00000000..9dbd4bd9 --- /dev/null +++ b/protocol/proto/LocalFilesSourceReport.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message LocalFilesSourceReport { + optional string id = 1; + optional int64 tracks = 2; +} diff --git a/protocol/proto/MdnsLoginFailures.proto b/protocol/proto/MdnsLoginFailures.proto new file mode 100644 index 00000000..cd036561 --- /dev/null +++ b/protocol/proto/MdnsLoginFailures.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message MdnsLoginFailures { + optional string device_id = 1; + optional int64 error_code = 2; + optional string response_body = 3; + optional string request = 4; + optional int64 esdk_internal_error_code = 5; + optional string context = 6; +} diff --git a/protocol/proto/MercuryCacheReport.proto b/protocol/proto/MercuryCacheReport.proto new file mode 100644 index 00000000..4c9e494f --- /dev/null +++ b/protocol/proto/MercuryCacheReport.proto @@ -0,0 +1,20 @@ +// 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/MetadataExtensionClientStatistic.proto b/protocol/proto/MetadataExtensionClientStatistic.proto new file mode 100644 index 00000000..253e0e18 --- /dev/null +++ b/protocol/proto/MetadataExtensionClientStatistic.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message MetadataExtensionClientStatistic { + optional bytes task_id = 1; + optional string feature_id = 2; + optional bool is_online_param = 3; + optional int32 num_extensions_with_etags = 4; + optional int32 num_extensions_requested = 5; + optional int32 num_extensions_needed = 6; + optional int32 num_uris_requested = 7; + optional int32 num_uris_needed = 8; + optional int32 num_prepared_requests = 9; + optional int32 num_sent_requests = 10; +} diff --git a/protocol/proto/ModuleDebug.proto b/protocol/proto/ModuleDebug.proto new file mode 100644 index 00000000..87691cd4 --- /dev/null +++ b/protocol/proto/ModuleDebug.proto @@ -0,0 +1,11 @@ +// 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/Offline2ClientError.proto b/protocol/proto/Offline2ClientError.proto new file mode 100644 index 00000000..55c9ca24 --- /dev/null +++ b/protocol/proto/Offline2ClientError.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message Offline2ClientError { + optional string error = 1; + optional string device_id = 2; + optional string cache_id = 3; +} diff --git a/protocol/proto/Offline2ClientEvent.proto b/protocol/proto/Offline2ClientEvent.proto new file mode 100644 index 00000000..b45bfd59 --- /dev/null +++ b/protocol/proto/Offline2ClientEvent.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message Offline2ClientEvent { + optional string event = 1; + optional string device_id = 2; + optional string cache_id = 3; +} diff --git a/protocol/proto/OfflineError.proto b/protocol/proto/OfflineError.proto new file mode 100644 index 00000000..e669ce43 --- /dev/null +++ b/protocol/proto/OfflineError.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message OfflineError { + optional int64 error_code = 1; + optional string track = 2; +} diff --git a/protocol/proto/OfflineEvent.proto b/protocol/proto/OfflineEvent.proto new file mode 100644 index 00000000..e924f093 --- /dev/null +++ b/protocol/proto/OfflineEvent.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message OfflineEvent { + optional string event = 1; + optional string data = 2; +} diff --git a/protocol/proto/OfflineReport.proto b/protocol/proto/OfflineReport.proto new file mode 100644 index 00000000..2835f77d --- /dev/null +++ b/protocol/proto/OfflineReport.proto @@ -0,0 +1,26 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message OfflineReport { + optional int64 total_num_tracks = 1; + optional int64 num_downloaded_tracks = 2; + optional int64 num_downloaded_tracks_keyless = 3; + optional int64 total_num_links = 4; + optional int64 total_num_links_keyless = 5; + map context_num_links_map = 6; + map linktype_num_tracks_map = 7; + optional int64 track_limit = 8; + optional int64 expiry = 9; + optional string change_reason = 10; + optional int64 offline_keys = 11; + optional int64 cached_keys = 12; + optional int64 total_num_episodes = 13; + optional int64 num_downloaded_episodes = 14; + optional int64 episode_limit = 15; + optional int64 episode_expiry = 16; +} diff --git a/protocol/proto/OfflineUserPwdLoginNonAuth.proto b/protocol/proto/OfflineUserPwdLoginNonAuth.proto new file mode 100644 index 00000000..2932bd56 --- /dev/null +++ b/protocol/proto/OfflineUserPwdLoginNonAuth.proto @@ -0,0 +1,11 @@ +// 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/PlaybackError.proto b/protocol/proto/PlaybackError.proto new file mode 100644 index 00000000..6897490e --- /dev/null +++ b/protocol/proto/PlaybackError.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message PlaybackError { + optional bytes file_id = 1; + optional bytes playback_id = 2; + optional string track_id = 3; + optional int64 bitrate = 4; + optional int64 error_code = 5; + optional bool fatal = 6; + optional string audiocodec = 7; + optional bool external_track = 8; + optional int64 position_ms = 9; +} diff --git a/protocol/proto/PlaybackRetry.proto b/protocol/proto/PlaybackRetry.proto new file mode 100644 index 00000000..82b9e9b3 --- /dev/null +++ b/protocol/proto/PlaybackRetry.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message PlaybackRetry { + optional string track = 1; + optional bytes playback_id = 2; + optional string method = 3; + optional string status = 4; + optional string reason = 5; +} diff --git a/protocol/proto/PlaybackSegments.proto b/protocol/proto/PlaybackSegments.proto new file mode 100644 index 00000000..bd5026c7 --- /dev/null +++ b/protocol/proto/PlaybackSegments.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message PlaybackSegments { + optional bytes playback_id = 1; + optional string track_uri = 2; + optional bool overflow = 3; + optional string segments = 4; +} diff --git a/protocol/proto/PlayerStateRestore.proto b/protocol/proto/PlayerStateRestore.proto new file mode 100644 index 00000000..f9778a7a --- /dev/null +++ b/protocol/proto/PlayerStateRestore.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message PlayerStateRestore { + optional string error = 1; + optional int64 size = 2; + optional string context_uri = 3; + optional string state = 4; +} diff --git a/protocol/proto/PlaylistSyncEvent.proto b/protocol/proto/PlaylistSyncEvent.proto new file mode 100644 index 00000000..6f2a23e2 --- /dev/null +++ b/protocol/proto/PlaylistSyncEvent.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message PlaylistSyncEvent { + optional string playlist_id = 1; + optional bool is_playlist = 2; + optional int64 timestamp_ms = 3; + optional int32 error_code = 4; + optional string event_description = 5; +} diff --git a/protocol/proto/PodcastAdSegmentReceived.proto b/protocol/proto/PodcastAdSegmentReceived.proto new file mode 100644 index 00000000..036fb6d5 --- /dev/null +++ b/protocol/proto/PodcastAdSegmentReceived.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message PodcastAdSegmentReceived { + optional string episode_uri = 1; + optional string playback_id = 2; + optional string slots = 3; + optional bool is_audio = 4; +} diff --git a/protocol/proto/Prefetch.proto b/protocol/proto/Prefetch.proto new file mode 100644 index 00000000..c388668a --- /dev/null +++ b/protocol/proto/Prefetch.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message Prefetch { + optional int64 strategies = 1; + optional int64 strategy = 2; + optional bytes file_id = 3; + optional string track = 4; + optional int64 prefetch_index = 5; + optional int64 current_window_size = 6; + optional int64 max_window_size = 7; +} diff --git a/protocol/proto/PrefetchError.proto b/protocol/proto/PrefetchError.proto new file mode 100644 index 00000000..6a1e56b4 --- /dev/null +++ b/protocol/proto/PrefetchError.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message PrefetchError { + optional int64 strategy = 1; + optional string description = 2; +} diff --git a/protocol/proto/ProductStateUcsVerification.proto b/protocol/proto/ProductStateUcsVerification.proto new file mode 100644 index 00000000..95257538 --- /dev/null +++ b/protocol/proto/ProductStateUcsVerification.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ProductStateUcsVerification { + map additional_entries = 1; + map missing_entries = 2; + optional string fetch_type = 3; +} diff --git a/protocol/proto/PubSubCountPerIdent.proto b/protocol/proto/PubSubCountPerIdent.proto new file mode 100644 index 00000000..a2d1e097 --- /dev/null +++ b/protocol/proto/PubSubCountPerIdent.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message PubSubCountPerIdent { + optional string ident_filter = 1; + optional int32 no_of_messages_received = 2; + optional int32 no_of_failed_conversions = 3; +} diff --git a/protocol/proto/ReachabilityChanged.proto b/protocol/proto/ReachabilityChanged.proto new file mode 100644 index 00000000..d8e3bc10 --- /dev/null +++ b/protocol/proto/ReachabilityChanged.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ReachabilityChanged { + optional string type = 1; + optional string info = 2; +} diff --git a/protocol/proto/RejectedClientEventNonAuth.proto b/protocol/proto/RejectedClientEventNonAuth.proto new file mode 100644 index 00000000..d592809b --- /dev/null +++ b/protocol/proto/RejectedClientEventNonAuth.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message RejectedClientEventNonAuth { + optional string reject_reason = 1; + optional string event_name = 2; +} diff --git a/protocol/proto/RemainingSkips.proto b/protocol/proto/RemainingSkips.proto new file mode 100644 index 00000000..d6ceebc0 --- /dev/null +++ b/protocol/proto/RemainingSkips.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message RemainingSkips { + optional string interaction_id = 1; + optional int32 remaining_skips_before_skip = 2; + optional int32 remaining_skips_after_skip = 3; + repeated string interaction_ids = 4; +} diff --git a/protocol/proto/RequestAccounting.proto b/protocol/proto/RequestAccounting.proto new file mode 100644 index 00000000..897cffb9 --- /dev/null +++ b/protocol/proto/RequestAccounting.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message RequestAccounting { + optional string request = 1; + optional int64 downloaded = 2; + optional int64 uploaded = 3; + optional int64 num_requests = 4; + optional string connection = 5; + optional string source_identifier = 6; + optional string reason = 7; + optional int64 duration_ms = 8; +} diff --git a/protocol/proto/RequestTime.proto b/protocol/proto/RequestTime.proto new file mode 100644 index 00000000..f0b7134f --- /dev/null +++ b/protocol/proto/RequestTime.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message RequestTime { + optional string type = 1; + optional int64 first_byte = 2; + optional int64 last_byte = 3; + optional int64 size = 4; + optional int64 size_sent = 5; + optional bool error = 6; + optional string url = 7; + optional string verb = 8; + optional int64 payload_size_sent = 9; + optional int32 connection_reuse = 10; + optional double sampling_probability = 11; + optional bool cached = 12; +} diff --git a/protocol/proto/StartTrack.proto b/protocol/proto/StartTrack.proto new file mode 100644 index 00000000..5bbf5273 --- /dev/null +++ b/protocol/proto/StartTrack.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message StartTrack { + optional bytes playback_id = 1; + optional string context_player_session_id = 2; + optional int64 timestamp = 3; +} diff --git a/protocol/proto/Stutter.proto b/protocol/proto/Stutter.proto new file mode 100644 index 00000000..bd0b2980 --- /dev/null +++ b/protocol/proto/Stutter.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message Stutter { + optional bytes file_id = 1; + optional bytes playback_id = 2; + optional string track = 3; + optional int64 buffer_size = 4; + optional int64 max_buffer_size = 5; + optional int64 file_byte_offset = 6; + optional int64 file_byte_total = 7; + optional int64 target_buffer = 8; + optional string audio_driver = 9; +} diff --git a/protocol/proto/TierFeatureFlags.proto b/protocol/proto/TierFeatureFlags.proto new file mode 100644 index 00000000..01f4311f --- /dev/null +++ b/protocol/proto/TierFeatureFlags.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message TierFeatureFlags { + optional bool ads = 1; + optional bool high_quality = 2; + optional bool offline = 3; + optional bool on_demand = 4; + optional string max_album_plays_consecutive = 5; + optional string max_album_plays_per_hour = 6; + optional string max_skips_per_hour = 7; + optional string max_track_plays_per_hour = 8; +} diff --git a/protocol/proto/TrackNotPlayed.proto b/protocol/proto/TrackNotPlayed.proto new file mode 100644 index 00000000..58c3ead2 --- /dev/null +++ b/protocol/proto/TrackNotPlayed.proto @@ -0,0 +1,24 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message TrackNotPlayed { + optional bytes playback_id = 1; + optional string source_start = 2; + optional string reason_start = 3; + optional string source_end = 4; + optional string reason_end = 5; + optional string play_context = 6; + optional string play_track = 7; + optional string display_track = 8; + optional string provider = 9; + optional string referer = 10; + optional string referrer_version = 11; + optional string referrer_vendor = 12; + optional string gaia_dev_id = 13; + optional string reason_not_played = 14; +} diff --git a/protocol/proto/TrackStuck.proto b/protocol/proto/TrackStuck.proto new file mode 100644 index 00000000..566d6494 --- /dev/null +++ b/protocol/proto/TrackStuck.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message TrackStuck { + optional string track = 1; + optional bytes playback_id = 2; + optional string source_start = 3; + optional string reason_start = 4; + optional bool offline = 5; + optional int64 position = 6; + optional int64 count = 7; + optional string audio_driver = 8; +} diff --git a/protocol/proto/WindowSize.proto b/protocol/proto/WindowSize.proto new file mode 100644 index 00000000..7860b1e7 --- /dev/null +++ b/protocol/proto/WindowSize.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message WindowSize { + optional int64 width = 1; + optional int64 height = 2; + optional int64 mode = 3; + optional int64 duration = 4; +} diff --git a/protocol/proto/ad-hermes-proxy.proto b/protocol/proto/ad-hermes-proxy.proto deleted file mode 100644 index 219bbcbf..00000000 --- a/protocol/proto/ad-hermes-proxy.proto +++ /dev/null @@ -1,51 +0,0 @@ -syntax = "proto2"; - -message Rule { - optional string type = 0x1; - optional uint32 times = 0x2; - optional uint64 interval = 0x3; -} - -message AdRequest { - optional string client_language = 0x1; - optional string product = 0x2; - optional uint32 version = 0x3; - optional string type = 0x4; - repeated string avoidAds = 0x5; -} - -message AdQueueResponse { - repeated AdQueueEntry adQueueEntry = 0x1; -} - -message AdFile { - optional string id = 0x1; - optional string format = 0x2; -} - -message AdQueueEntry { - optional uint64 start_time = 0x1; - optional uint64 end_time = 0x2; - optional double priority = 0x3; - optional string token = 0x4; - optional uint32 ad_version = 0x5; - optional string id = 0x6; - optional string type = 0x7; - optional string campaign = 0x8; - optional string advertiser = 0x9; - optional string url = 0xa; - optional uint64 duration = 0xb; - optional uint64 expiry = 0xc; - optional string tracking_url = 0xd; - optional string banner_type = 0xe; - optional string html = 0xf; - optional string image = 0x10; - optional string background_image = 0x11; - optional string background_url = 0x12; - optional string background_color = 0x13; - optional string title = 0x14; - optional string caption = 0x15; - repeated AdFile file = 0x16; - repeated Rule rule = 0x17; -} - diff --git a/protocol/proto/anchor_extended_metadata.proto b/protocol/proto/anchor_extended_metadata.proto new file mode 100644 index 00000000..24d715a3 --- /dev/null +++ b/protocol/proto/anchor_extended_metadata.proto @@ -0,0 +1,14 @@ +// 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 new file mode 100644 index 00000000..deffc3d6 --- /dev/null +++ b/protocol/proto/apiv1.proto @@ -0,0 +1,113 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.offline.proto; + +import "google/protobuf/timestamp.proto"; +import "offline.proto"; + +option optimize_for = CODE_SIZE; + +message ListDevicesRequest { + string user_id = 1; +} + +message ListDevicesResponse { + repeated Device devices = 1; +} + +message PutDeviceRequest { + string user_id = 1; + + Body body = 2; + message Body { + Device device = 1; + } +} + +message BasicDeviceRequest { + DeviceKey key = 1; +} + +message GetDeviceResponse { + Device device = 1; +} + +message RemoveDeviceRequest { + DeviceKey key = 1; + bool is_force_remove = 2; +} + +message RemoveDeviceResponse { + bool pending = 1; + Device device = 2; +} + +message OfflineEnableDeviceResponse { + Restrictions restrictions = 1; +} + +message ListResourcesResponse { + repeated Resource resources = 1; + google.protobuf.Timestamp server_time = 2; +} + +message WriteResourcesRequest { + DeviceKey key = 1; + + Body body = 2; + message Body { + repeated ResourceOperation operations = 1; + string source_device_id = 2; + string source_cache_id = 3; + } +} + +message ResourcesUpdate { + string source_device_id = 1; + string source_cache_id = 2; +} + +message DeltaResourcesRequest { + DeviceKey key = 1; + + Body body = 2; + message Body { + google.protobuf.Timestamp last_known_server_time = 1; + } +} + +message DeltaResourcesResponse { + bool delta_update_possible = 1; + repeated ResourceOperation operations = 2; + google.protobuf.Timestamp server_time = 3; +} + +message GetResourceRequest { + DeviceKey key = 1; + string uri = 2; +} + +message GetResourceResponse { + Resource resource = 1; +} + +message WriteResourcesDetailsRequest { + DeviceKey key = 1; + + Body body = 2; + message Body { + repeated Resource resources = 1; + } +} + +message GetResourceForDevicesRequest { + string user_id = 1; + string uri = 2; +} + +message GetResourceForDevicesResponse { + repeated Device devices = 1; + repeated ResourceForDevice resources = 2; +} diff --git a/protocol/proto/appstore.proto b/protocol/proto/appstore.proto deleted file mode 100644 index bddaaf30..00000000 --- a/protocol/proto/appstore.proto +++ /dev/null @@ -1,95 +0,0 @@ -syntax = "proto2"; - -message AppInfo { - optional string identifier = 0x1; - optional int32 version_int = 0x2; -} - -message AppInfoList { - repeated AppInfo items = 0x1; -} - -message SemanticVersion { - optional int32 major = 0x1; - optional int32 minor = 0x2; - optional int32 patch = 0x3; -} - -message RequestHeader { - optional string market = 0x1; - optional Platform platform = 0x2; - enum Platform { - WIN32_X86 = 0x0; - OSX_X86 = 0x1; - LINUX_X86 = 0x2; - IPHONE_ARM = 0x3; - SYMBIANS60_ARM = 0x4; - OSX_POWERPC = 0x5; - ANDROID_ARM = 0x6; - WINCE_ARM = 0x7; - LINUX_X86_64 = 0x8; - OSX_X86_64 = 0x9; - PALM_ARM = 0xa; - LINUX_SH = 0xb; - FREEBSD_X86 = 0xc; - FREEBSD_X86_64 = 0xd; - BLACKBERRY_ARM = 0xe; - SONOS_UNKNOWN = 0xf; - LINUX_MIPS = 0x10; - LINUX_ARM = 0x11; - LOGITECH_ARM = 0x12; - LINUX_BLACKFIN = 0x13; - ONKYO_ARM = 0x15; - QNXNTO_ARM = 0x16; - BADPLATFORM = 0xff; - } - optional AppInfoList app_infos = 0x6; - optional string bridge_identifier = 0x7; - optional SemanticVersion bridge_version = 0x8; - optional DeviceClass device_class = 0x9; - enum DeviceClass { - DESKTOP = 0x1; - TABLET = 0x2; - MOBILE = 0x3; - WEB = 0x4; - TV = 0x5; - } -} - -message AppItem { - optional string identifier = 0x1; - optional Requirement requirement = 0x2; - enum Requirement { - REQUIRED_INSTALL = 0x1; - LAZYLOAD = 0x2; - OPTIONAL_INSTALL = 0x3; - } - optional string manifest = 0x4; - optional string checksum = 0x5; - optional string bundle_uri = 0x6; - optional string small_icon_uri = 0x7; - optional string large_icon_uri = 0x8; - optional string medium_icon_uri = 0x9; - optional Type bundle_type = 0xa; - enum Type { - APPLICATION = 0x0; - FRAMEWORK = 0x1; - BRIDGE = 0x2; - } - optional SemanticVersion version = 0xb; - optional uint32 ttl_in_seconds = 0xc; - optional IdentifierList categories = 0xd; -} - -message AppList { - repeated AppItem items = 0x1; -} - -message IdentifierList { - repeated string identifiers = 0x1; -} - -message BannerConfig { - optional string json = 0x1; -} - diff --git a/protocol/proto/audio_files_extension.proto b/protocol/proto/audio_files_extension.proto new file mode 100644 index 00000000..32efd995 --- /dev/null +++ b/protocol/proto/audio_files_extension.proto @@ -0,0 +1,27 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.extendedmetadata.audiofiles; + +import "metadata.proto"; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.audiophile.proto"; + +message NormalizationParams { + float loudness_db = 1; + float true_peak_db = 2; +} + +message ExtendedAudioFile { + metadata.AudioFile file = 1; + NormalizationParams file_normalization_params = 2; + NormalizationParams album_normalization_params = 3; +} + +message AudioFilesExtensionResponse { + repeated ExtendedAudioFile files = 1; + NormalizationParams default_file_normalization_params = 2; + NormalizationParams default_album_normalization_params = 3; +} diff --git a/protocol/proto/automix_mode.proto b/protocol/proto/automix_mode.proto new file mode 100644 index 00000000..a4d7d66f --- /dev/null +++ b/protocol/proto/automix_mode.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.automix.proto; + +option optimize_for = CODE_SIZE; + +message AutomixMode { + AutomixStyle style = 1; +} + +enum AutomixStyle { + NONE = 0; + DEFAULT = 1; + REGULAR = 2; + AIRBAG = 3; + RADIO_AIRBAG = 4; + SLEEP = 5; + MIXED = 6; +} diff --git a/protocol/proto/autoplay_context_request.proto b/protocol/proto/autoplay_context_request.proto new file mode 100644 index 00000000..4fa4b0bc --- /dev/null +++ b/protocol/proto/autoplay_context_request.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message AutoplayContextRequest { + required string context_uri = 1; + repeated string recent_track_uri = 2; +} diff --git a/protocol/proto/autoplay_node.proto b/protocol/proto/autoplay_node.proto new file mode 100644 index 00000000..18709f12 --- /dev/null +++ b/protocol/proto/autoplay_node.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +import "logging_params.proto"; + +option optimize_for = CODE_SIZE; + +message AutoplayNode { + map filler_node = 1; + required bool is_playing_filler = 2; + required LoggingParams logging_params = 3; +} diff --git a/protocol/proto/canvas.proto b/protocol/proto/canvas.proto new file mode 100644 index 00000000..e008618e --- /dev/null +++ b/protocol/proto/canvas.proto @@ -0,0 +1,33 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.context_track_exts.canvas; + +message Artist { + string uri = 1; + string name = 2; + string avatar = 3; +} + +message CanvasRecord { + string id = 1; + string url = 2; + string file_id = 3; + 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; +} + +enum Type { + IMAGE = 0; + VIDEO = 1; + VIDEO_LOOPING = 2; + VIDEO_LOOPING_RANDOM = 3; + GIF = 4; +} diff --git a/protocol/proto/capping_data.proto b/protocol/proto/capping_data.proto new file mode 100644 index 00000000..dca6353a --- /dev/null +++ b/protocol/proto/capping_data.proto @@ -0,0 +1,30 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.capper3; + +option java_multiple_files = true; +option java_package = "com.spotify.capper3.proto"; + +message ConsumeTokensRequest { + uint32 tokens = 1; +} + +message CappingData { + uint32 remaining_tokens = 1; + uint32 capacity = 2; + uint32 seconds_until_next_refill = 3; + uint32 refill_amount = 4; +} + +message ConsumeTokensResponse { + uint32 seconds_until_next_update = 1; + PlayCappingType capping_type = 2; + CappingData capping_data = 3; +} + +enum PlayCappingType { + NONE = 0; + LINEAR = 1; +} diff --git a/protocol/proto/claas.proto b/protocol/proto/claas.proto new file mode 100644 index 00000000..6006c17b --- /dev/null +++ b/protocol/proto/claas.proto @@ -0,0 +1,29 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.claas.v1; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.claas.v1"; + +service ClaasService { + rpc PostLogs(PostLogsRequest) returns (PostLogsResponse); + rpc Watch(WatchRequest) returns (stream WatchResponse); +} + +message WatchRequest { + string user_id = 1; +} + +message WatchResponse { + repeated string logs = 1; +} + +message PostLogsRequest { + repeated string logs = 1; +} + +message PostLogsResponse { + +} diff --git a/protocol/proto/client_update.proto b/protocol/proto/client_update.proto new file mode 100644 index 00000000..fb93c9bd --- /dev/null +++ b/protocol/proto/client_update.proto @@ -0,0 +1,39 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.desktopupdate.proto; + +option java_multiple_files = true; +option java_outer_classname = "ClientUpdateProto"; +option java_package = "com.spotify.desktopupdate.proto"; + +message UpgradeSignedPart { + uint32 platform = 1; + uint64 version_from_from = 2; + uint64 version_from_to = 3; + uint64 target_version = 4; + string http_prefix = 5; + bytes binary_hash = 6; + ClientUpgradeType type = 7; + bytes file_id = 8; + uint32 delay = 9; + uint32 flags = 10; +} + +message UpgradeRequiredMessage { + bytes upgrade_signed_part = 10; + bytes signature = 20; + string http_suffix = 30; +} + +message UpdateQueryResponse { + UpgradeRequiredMessage upgrade_message_payload = 1; + uint32 poll_interval = 2; +} + +enum ClientUpgradeType { + INVALID = 0; + LOGIN_CRITICAL = 1; + NORMAL = 2; +} diff --git a/protocol/proto/clips_cover.proto b/protocol/proto/clips_cover.proto new file mode 100644 index 00000000..b129fb4a --- /dev/null +++ b/protocol/proto/clips_cover.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.clips; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "ClipsCoverProto"; +option java_package = "com.spotify.clips.proto"; + +message ClipsCover { + string image_url = 1; + string video_source_id = 2; +} diff --git a/protocol/proto/cloud_host_messages.proto b/protocol/proto/cloud_host_messages.proto new file mode 100644 index 00000000..49949188 --- /dev/null +++ b/protocol/proto/cloud_host_messages.proto @@ -0,0 +1,152 @@ +// 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/album_collection_state.proto b/protocol/proto/collection/album_collection_state.proto new file mode 100644 index 00000000..1258961d --- /dev/null +++ b/protocol/proto/collection/album_collection_state.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message AlbumCollectionState { + optional string collection_link = 1; + optional uint32 num_tracks_in_collection = 2; + optional bool complete = 3; +} diff --git a/protocol/proto/collection/artist_collection_state.proto b/protocol/proto/collection/artist_collection_state.proto new file mode 100644 index 00000000..33ade56a --- /dev/null +++ b/protocol/proto/collection/artist_collection_state.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message ArtistCollectionState { + optional string collection_link = 1; + optional bool followed = 2; + optional uint32 num_tracks_in_collection = 3; + optional uint32 num_albums_in_collection = 4; + optional bool is_banned = 5; + optional bool can_ban = 6; +} diff --git a/protocol/proto/collection/episode_collection_state.proto b/protocol/proto/collection/episode_collection_state.proto new file mode 100644 index 00000000..403bfbb4 --- /dev/null +++ b/protocol/proto/collection/episode_collection_state.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message EpisodeCollectionState { + optional bool is_following_show = 1; + optional bool is_new = 2; + optional bool is_in_listen_later = 3; +} diff --git a/protocol/proto/collection/show_collection_state.proto b/protocol/proto/collection/show_collection_state.proto new file mode 100644 index 00000000..d3904b51 --- /dev/null +++ b/protocol/proto/collection/show_collection_state.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message ShowCollectionState { + optional bool is_in_collection = 1; +} diff --git a/protocol/proto/collection/track_collection_state.proto b/protocol/proto/collection/track_collection_state.proto new file mode 100644 index 00000000..68e42ed2 --- /dev/null +++ b/protocol/proto/collection/track_collection_state.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message TrackCollectionState { + optional bool is_in_collection = 1; + optional bool can_add_to_collection = 2; + optional bool is_banned = 3; + optional bool can_ban = 4; +} diff --git a/protocol/proto/collection2v2.proto b/protocol/proto/collection2v2.proto new file mode 100644 index 00000000..19530fe8 --- /dev/null +++ b/protocol/proto/collection2v2.proto @@ -0,0 +1,62 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.collection.proto.v2; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.collection2.v2.proto"; + +message PageRequest { + string username = 1; + string set = 2; + string pagination_token = 3; + int32 limit = 4; +} + +message CollectionItem { + string uri = 1; + int32 added_at = 2; + bool is_removed = 3; +} + +message PageResponse { + repeated CollectionItem items = 1; + string next_page_token = 2; + string sync_token = 3; +} + +message DeltaRequest { + string username = 1; + string set = 2; + string last_sync_token = 3; +} + +message DeltaResponse { + bool delta_update_possible = 1; + repeated CollectionItem items = 2; + string sync_token = 3; +} + +message WriteRequest { + string username = 1; + string set = 2; + repeated CollectionItem items = 3; + string client_update_id = 4; +} + +message PubSubUpdate { + string username = 1; + string set = 2; + repeated CollectionItem items = 3; + string client_update_id = 4; +} + +message InitializedRequest { + string username = 1; + string set = 2; +} + +message InitializedResponse { + bool initialized = 1; +} diff --git a/protocol/proto/collection_index.proto b/protocol/proto/collection_index.proto new file mode 100644 index 00000000..5af95a35 --- /dev/null +++ b/protocol/proto/collection_index.proto @@ -0,0 +1,40 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.collection.proto; + +option optimize_for = CODE_SIZE; + +message IndexRepairerState { + bytes last_checked_uri = 1; + int64 last_full_check_finished_at = 2; +} + +message CollectionTrackEntry { + string track_uri = 1; + string track_name = 2; + string album_uri = 3; + string album_name = 4; + int32 disc_number = 5; + int32 track_number = 6; + string artist_uri = 7; + repeated string artist_name = 8; + int64 add_time = 9; +} + +message CollectionAlbumEntry { + string album_uri = 1; + string album_name = 2; + string album_image_uri = 3; + string artist_uri = 4; + string artist_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; +} diff --git a/protocol/proto/collection_platform_requests.proto b/protocol/proto/collection_platform_requests.proto new file mode 100644 index 00000000..efe9a847 --- /dev/null +++ b/protocol/proto/collection_platform_requests.proto @@ -0,0 +1,24 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.collection_platform.proto; + +option optimize_for = CODE_SIZE; + +message CollectionPlatformSimpleRequest { + CollectionSet set = 1; +} + +message CollectionPlatformItemsRequest { + CollectionSet set = 1; + repeated string items = 2; +} + +enum CollectionSet { + UNKNOWN = 0; + SHOW = 1; + BAN = 2; + LISTENLATER = 3; + IGNOREINRECS = 4; +} diff --git a/protocol/proto/collection_platform_responses.proto b/protocol/proto/collection_platform_responses.proto new file mode 100644 index 00000000..fd236c12 --- /dev/null +++ b/protocol/proto/collection_platform_responses.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.collection_platform.proto; + +option optimize_for = CODE_SIZE; + +message CollectionPlatformSimpleResponse { + string error_msg = 1; +} + +message CollectionPlatformItemsResponse { + repeated string items = 1; +} + +message CollectionPlatformContainsResponse { + repeated bool found = 1; +} diff --git a/protocol/proto/collection_storage.proto b/protocol/proto/collection_storage.proto new file mode 100644 index 00000000..1dd4f034 --- /dev/null +++ b/protocol/proto/collection_storage.proto @@ -0,0 +1,20 @@ +// 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 new file mode 100644 index 00000000..75717c98 --- /dev/null +++ b/protocol/proto/composite_formats_node.proto @@ -0,0 +1,31 @@ +// 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/concat_cosmos.proto b/protocol/proto/concat_cosmos.proto new file mode 100644 index 00000000..7fe045a8 --- /dev/null +++ b/protocol/proto/concat_cosmos.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.concat_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message ConcatRequest { + string a = 1; + string b = 2; +} + +message ConcatWithSeparatorRequest { + string a = 1; + string b = 2; + string separator = 3; +} + +message ConcatResponse { + string concatenated = 1; +} diff --git a/protocol/proto/connect.proto b/protocol/proto/connect.proto new file mode 100644 index 00000000..310a5b55 --- /dev/null +++ b/protocol/proto/connect.proto @@ -0,0 +1,235 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.connectstate; + +import "player.proto"; +import "devices.proto"; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.connectstate.model"; + +message ClusterUpdate { + Cluster cluster = 1; + ClusterUpdateReason update_reason = 2; + string ack_id = 3; + repeated string devices_that_changed = 4; +} + +message PostCommandResponse { + string ack_id = 1; +} + +message Device { + DeviceInfo device_info = 1; + PlayerState player_state = 2; + PrivateDeviceInfo private_device_info = 3; + bytes transfer_data = 4; +} + +message Cluster { + int64 changed_timestamp_ms = 1; + string active_device_id = 2; + PlayerState player_state = 3; + map device = 4; + bytes transfer_data = 5; + uint64 transfer_data_timestamp = 6; + int64 not_playing_since_timestamp = 7; + bool need_full_player_state = 8; + int64 server_timestamp_ms = 9; +} + +message PutStateRequest { + string callback_url = 1; + Device device = 2; + MemberType member_type = 3; + bool is_active = 4; + PutStateReason put_state_reason = 5; + uint32 message_id = 6; + string last_command_sent_by_device_id = 7; + uint32 last_command_message_id = 8; + uint64 started_playing_at = 9; + uint64 has_been_playing_for_ms = 11; + uint64 client_side_timestamp = 12; + bool only_write_player_state = 13; +} + +message PrivateDeviceInfo { + string platform = 1; +} + +message SubscribeRequest { + string callback_url = 1; +} + +message DeviceInfo { + bool can_play = 1; + uint32 volume = 2; + string name = 3; + Capabilities capabilities = 4; + repeated DeviceMetadata metadata = 5; + string device_software_version = 6; + devices.DeviceType device_type = 7; + string spirc_version = 9; + string device_id = 10; + bool is_private_session = 11; + bool is_social_connect = 12; + string client_id = 13; + string brand = 14; + string model = 15; + map metadata_map = 16; + string product_id = 17; + string deduplication_id = 18; + uint32 selected_alias_id = 19; + map device_aliases = 20; + bool is_offline = 21; + string public_ip = 22; + string license = 23; + bool is_group = 25; + + oneof _audio_output_device_info { + AudioOutputDeviceInfo audio_output_device_info = 24; + } +} + +message AudioOutputDeviceInfo { + oneof _audio_output_device_type { + AudioOutputDeviceType audio_output_device_type = 1; + } + + oneof _device_name { + string device_name = 2; + } +} + +message DeviceMetadata { + option deprecated = true; + string type = 1; + string metadata = 2; +} + +message Capabilities { + bool can_be_player = 2; + bool restrict_to_local = 3; + bool gaia_eq_connect_id = 5; + bool supports_logout = 6; + bool is_observable = 7; + int32 volume_steps = 8; + repeated string supported_types = 9; + bool command_acks = 10; + bool supports_rename = 11; + bool hidden = 12; + bool disable_volume = 13; + bool connect_disabled = 14; + bool supports_playlist_v2 = 15; + bool is_controllable = 16; + bool supports_external_episodes = 17; + bool supports_set_backend_metadata = 18; + bool supports_transfer_command = 19; + bool supports_command_request = 20; + bool is_voice_enabled = 21; + bool needs_full_player_state = 22; + bool supports_gzip_pushes = 23; + bool supports_set_options_command = 25; + CapabilitySupportDetails supports_hifi = 26; + + reserved 1, 4, 24, "supported_contexts", "supports_lossless_audio"; +} + +message CapabilitySupportDetails { + bool fully_supported = 1; + bool user_eligible = 2; + bool device_supported = 3; +} + +message ConnectCommandOptions { + int32 message_id = 1; + uint32 target_alias_id = 3; +} + +message LogoutCommand { + ConnectCommandOptions command_options = 1; +} + +message SetVolumeCommand { + int32 volume = 1; + ConnectCommandOptions command_options = 2; +} + +message RenameCommand { + string rename_to = 1; + ConnectCommandOptions command_options = 2; +} + +message ConnectPlayerCommand { + string player_command_json = 1; + ConnectCommandOptions command_options = 2; +} + +message SetBackendMetadataCommand { + map metadata = 1; +} + +message CommandAndSourceDevice { + string command = 1; + DeviceInfo source_device_info = 2; +} + +message ActiveDeviceUpdate { + string device_id = 1; +} + +message StartedPlayingEvent { + bytes user_info_header = 1; + string device_id = 2; +} + +enum AudioOutputDeviceType { + UNKNOWN_AUDIO_OUTPUT_DEVICE_TYPE = 0; + BUILT_IN_SPEAKER = 1; + LINE_OUT = 2; + BLUETOOTH = 3; + AIRPLAY = 4; +} + +enum PutStateReason { + UNKNOWN_PUT_STATE_REASON = 0; + SPIRC_HELLO = 1; + SPIRC_NOTIFY = 2; + NEW_DEVICE = 3; + PLAYER_STATE_CHANGED = 4; + VOLUME_CHANGED = 5; + PICKER_OPENED = 6; + BECAME_INACTIVE = 7; + ALIAS_CHANGED = 8; +} + +enum MemberType { + SPIRC_V2 = 0; + SPIRC_V3 = 1; + CONNECT_STATE = 2; + CONNECT_STATE_EXTENDED = 5; + ACTIVE_DEVICE_TRACKER = 6; + PLAY_TOKEN = 7; +} + +enum ClusterUpdateReason { + UNKNOWN_CLUSTER_UPDATE_REASON = 0; + DEVICES_DISAPPEARED = 1; + DEVICE_STATE_CHANGED = 2; + NEW_DEVICE_APPEARED = 3; + DEVICE_VOLUME_CHANGED = 4; + DEVICE_ALIAS_CHANGED = 5; +} + +enum SendCommandResult { + UNKNOWN_SEND_COMMAND_RESULT = 0; + SUCCESS = 1; + DEVICE_NOT_FOUND = 2; + CONTEXT_PLAYER_ERROR = 3; + DEVICE_DISAPPEARED = 4; + UPSTREAM_ERROR = 5; + DEVICE_DOES_NOT_SUPPORT_COMMAND = 6; + RATE_LIMITED = 7; +} diff --git a/protocol/proto/connectivity.proto b/protocol/proto/connectivity.proto new file mode 100644 index 00000000..f7e64a3c --- /dev/null +++ b/protocol/proto/connectivity.proto @@ -0,0 +1,43 @@ +// Extracted from: Spotify 1.1.33.569 (Windows) + +syntax = "proto3"; + +package spotify.clienttoken.data.v0; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.clienttoken.data.v0"; + +message ConnectivitySdkData { + PlatformSpecificData platform_specific_data = 1; + string device_id = 2; +} + +message PlatformSpecificData { + oneof data { + NativeAndroidData android = 1; + NativeIOSData ios = 2; + } +} + +message NativeAndroidData { + int32 major_sdk_version = 1; + int32 minor_sdk_version = 2; + int32 patch_sdk_version = 3; + uint32 api_version = 4; + Screen screen_dimensions = 5; +} + +message NativeIOSData { + int32 user_interface_idiom = 1; + bool target_iphone_simulator = 2; + string hw_machine = 3; + string system_version = 4; + string simulator_model_identifier = 5; +} + +message Screen { + int32 width = 1; + int32 height = 2; + int32 density = 3; +} diff --git a/protocol/proto/contains_request.proto b/protocol/proto/contains_request.proto new file mode 100644 index 00000000..cf59c5f5 --- /dev/null +++ b/protocol/proto/contains_request.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +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 ContainsRequest { + repeated string items = 1; +} + +message ContainsResponse { + repeated bool found = 1; +} diff --git a/protocol/proto/content_access_token_cosmos.proto b/protocol/proto/content_access_token_cosmos.proto new file mode 100644 index 00000000..2c98125b --- /dev/null +++ b/protocol/proto/content_access_token_cosmos.proto @@ -0,0 +1,36 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.contentaccesstoken.proto; + +import "google/protobuf/timestamp.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.contentaccesstoken.proto"; + +message ContentAccessTokenResponse { + Error error = 1; + ContentAccessToken content_access_token = 2; +} + +message ContentAccessToken { + string token = 1; + google.protobuf.Timestamp expires_at = 2; + google.protobuf.Timestamp refresh_at = 3; + repeated string domains = 4; +} + +message ContentAccessRefreshToken { + string token = 1; +} + +message IsEnabledResponse { + bool is_enabled = 1; +} + +message Error { + int32 error_code = 1; + string error_description = 2; +} diff --git a/protocol/proto/context.proto b/protocol/proto/context.proto new file mode 100644 index 00000000..eb022415 --- /dev/null +++ b/protocol/proto/context.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +import "context_page.proto"; +import "restrictions.proto"; + +option optimize_for = CODE_SIZE; + +message Context { + optional string uri = 1; + optional string url = 2; + map metadata = 3; + optional Restrictions restrictions = 4; + repeated ContextPage pages = 5; + optional bool loading = 6; +} diff --git a/protocol/proto/context_client_id.proto b/protocol/proto/context_client_id.proto new file mode 100644 index 00000000..bab3b6b8 --- /dev/null +++ b/protocol/proto/context_client_id.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message ClientId { + bytes value = 1; +} diff --git a/protocol/proto/context_core.proto b/protocol/proto/context_core.proto new file mode 100644 index 00000000..1e49afaf --- /dev/null +++ b/protocol/proto/context_core.proto @@ -0,0 +1,14 @@ +// 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_index.proto b/protocol/proto/context_index.proto new file mode 100644 index 00000000..c7049eac --- /dev/null +++ b/protocol/proto/context_index.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message ContextIndex { + optional uint32 page = 1; + optional uint32 track = 2; +} diff --git a/protocol/proto/context_installation_id.proto b/protocol/proto/context_installation_id.proto new file mode 100644 index 00000000..08fe2580 --- /dev/null +++ b/protocol/proto/context_installation_id.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message InstallationId { + bytes value = 1; +} diff --git a/protocol/proto/context_monotonic_clock.proto b/protocol/proto/context_monotonic_clock.proto new file mode 100644 index 00000000..3ec525ff --- /dev/null +++ b/protocol/proto/context_monotonic_clock.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message MonotonicClock { + int64 id = 1; + int64 value = 2; +} diff --git a/protocol/proto/context_node.proto b/protocol/proto/context_node.proto new file mode 100644 index 00000000..8ff3cb28 --- /dev/null +++ b/protocol/proto/context_node.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +import "context_processor.proto"; +import "play_origin.proto"; +import "prepare_play_options.proto"; +import "track_instance.proto"; +import "track_instantiator.proto"; + +option optimize_for = CODE_SIZE; + +message ContextNode { + optional TrackInstance current_track = 2; + optional TrackInstantiator instantiate = 3; + optional PreparePlayOptions prepare_options = 4; + optional PlayOrigin play_origin = 5; + optional ContextProcessor context_processor = 6; + optional string session_id = 7; + optional sint32 iteration = 8; +} diff --git a/protocol/proto/context_page.proto b/protocol/proto/context_page.proto new file mode 100644 index 00000000..b6e8ecdc --- /dev/null +++ b/protocol/proto/context_page.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +import "context_track.proto"; + +option optimize_for = CODE_SIZE; + +message ContextPage { + optional string page_url = 1; + optional string next_page_url = 2; + map metadata = 3; + repeated ContextTrack tracks = 4; + optional bool loading = 5; +} diff --git a/protocol/proto/context_player_ng.proto b/protocol/proto/context_player_ng.proto new file mode 100644 index 00000000..e61f011e --- /dev/null +++ b/protocol/proto/context_player_ng.proto @@ -0,0 +1,12 @@ +// 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_player_options.proto b/protocol/proto/context_player_options.proto new file mode 100644 index 00000000..57e069b5 --- /dev/null +++ b/protocol/proto/context_player_options.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message ContextPlayerOptions { + optional bool shuffling_context = 1; + optional bool repeating_context = 2; + optional bool repeating_track = 3; +} + +message ContextPlayerOptionOverrides { + optional bool shuffling_context = 1; + optional bool repeating_context = 2; + optional bool repeating_track = 3; +} diff --git a/protocol/proto/context_processor.proto b/protocol/proto/context_processor.proto new file mode 100644 index 00000000..2d931b0b --- /dev/null +++ b/protocol/proto/context_processor.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +import "context.proto"; +import "context_view.proto"; +import "skip_to_track.proto"; + +option optimize_for = CODE_SIZE; + +message ContextProcessor { + optional Context context = 1; + optional context_view.proto.ContextView context_view = 2; + optional SkipToTrack pending_skip_to = 3; + optional string shuffle_seed = 4; + optional int32 index = 5; +} diff --git a/protocol/proto/context_sdk.proto b/protocol/proto/context_sdk.proto new file mode 100644 index 00000000..dc5d3236 --- /dev/null +++ b/protocol/proto/context_sdk.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message Sdk { + string version_name = 1; +} diff --git a/protocol/proto/context_time.proto b/protocol/proto/context_time.proto new file mode 100644 index 00000000..93749b41 --- /dev/null +++ b/protocol/proto/context_time.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message Time { + int64 value = 1; +} diff --git a/protocol/proto/context_track.proto b/protocol/proto/context_track.proto new file mode 100644 index 00000000..e9d06f21 --- /dev/null +++ b/protocol/proto/context_track.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message ContextTrack { + optional string uri = 1; + optional string uid = 2; + optional bytes gid = 3; + map metadata = 4; +} diff --git a/protocol/proto/context_view.proto b/protocol/proto/context_view.proto new file mode 100644 index 00000000..0b78991a --- /dev/null +++ b/protocol/proto/context_view.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.context_view.proto; + +import "context_track.proto"; +import "context_view_cyclic_list.proto"; + +option optimize_for = CODE_SIZE; + +message ContextView { + map patch_map = 1; + optional uint32 iteration_size = 2; + optional cyclic_list.proto.CyclicEntryKeyList cyclic_list = 3; + + reserved 4; +} diff --git a/protocol/proto/context_view_cyclic_list.proto b/protocol/proto/context_view_cyclic_list.proto new file mode 100644 index 00000000..76cde3ed --- /dev/null +++ b/protocol/proto/context_view_cyclic_list.proto @@ -0,0 +1,26 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.context_view.cyclic_list.proto; + +import "context_view_entry_key.proto"; + +option optimize_for = CODE_SIZE; + +message Instance { + optional context_view.proto.EntryKey item = 1; + optional int32 iteration = 2; +} + +message Patch { + optional int32 start = 1; + optional int32 end = 2; + repeated Instance instances = 3; +} + +message CyclicEntryKeyList { + optional context_view.proto.EntryKey delimiter = 1; + repeated context_view.proto.EntryKey items = 2; + optional Patch patch = 3; +} diff --git a/protocol/proto/context_view_entry.proto b/protocol/proto/context_view_entry.proto new file mode 100644 index 00000000..8451f481 --- /dev/null +++ b/protocol/proto/context_view_entry.proto @@ -0,0 +1,25 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.context_view.proto; + +import "context_index.proto"; +import "context_track.proto"; + +option optimize_for = CODE_SIZE; + +message Entry { + optional Type type = 1; + enum Type { + TRACK = 0; + DELIMITER = 1; + PAGE_PLACEHOLDER = 2; + CONTEXT_PLACEHOLDER = 3; + } + + optional player.proto.ContextTrack track = 2; + optional player.proto.ContextIndex index = 3; + optional int32 page_index = 4; + optional int32 absolute_index = 5; +} diff --git a/protocol/proto/context_view_entry_key.proto b/protocol/proto/context_view_entry_key.proto new file mode 100644 index 00000000..6c8a019f --- /dev/null +++ b/protocol/proto/context_view_entry_key.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.context_view.proto; + +import "context_view_entry.proto"; + +option optimize_for = CODE_SIZE; + +message EntryKey { + optional Entry.Type type = 1; + optional string data = 2; +} diff --git a/protocol/proto/core_configuration_applied_non_auth.proto b/protocol/proto/core_configuration_applied_non_auth.proto new file mode 100644 index 00000000..d7c132dc --- /dev/null +++ b/protocol/proto/core_configuration_applied_non_auth.proto @@ -0,0 +1,11 @@ +// 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 new file mode 100644 index 00000000..47cd584f --- /dev/null +++ b/protocol/proto/cosmos_changes_request.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.collection_cosmos.changes_request.proto; + +option optimize_for = CODE_SIZE; + +message Response { + +} diff --git a/protocol/proto/cosmos_decorate_request.proto b/protocol/proto/cosmos_decorate_request.proto new file mode 100644 index 00000000..2709b30a --- /dev/null +++ b/protocol/proto/cosmos_decorate_request.proto @@ -0,0 +1,70 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.collection_cosmos.decorate_request.proto; + +import "collection/album_collection_state.proto"; +import "collection/artist_collection_state.proto"; +import "collection/episode_collection_state.proto"; +import "collection/show_collection_state.proto"; +import "collection/track_collection_state.proto"; +import "played_state/episode_played_state.proto"; +import "played_state/show_played_state.proto"; +import "played_state/track_played_state.proto"; +import "sync/album_sync_state.proto"; +import "sync/artist_sync_state.proto"; +import "sync/episode_sync_state.proto"; +import "sync/track_sync_state.proto"; +import "metadata/album_metadata.proto"; +import "metadata/artist_metadata.proto"; +import "metadata/episode_metadata.proto"; +import "metadata/show_metadata.proto"; +import "metadata/track_metadata.proto"; + +option optimize_for = CODE_SIZE; + +message Album { + optional cosmos_util.proto.AlbumMetadata album_metadata = 1; + optional cosmos_util.proto.AlbumCollectionState album_collection_state = 2; + optional cosmos_util.proto.AlbumSyncState album_offline_state = 3; + optional string link = 4; +} + +message Artist { + optional cosmos_util.proto.ArtistMetadata artist_metadata = 1; + optional cosmos_util.proto.ArtistCollectionState artist_collection_state = 2; + optional cosmos_util.proto.ArtistSyncState artist_offline_state = 3; + optional string link = 4; +} + +message Episode { + optional cosmos_util.proto.EpisodeMetadata episode_metadata = 1; + optional cosmos_util.proto.EpisodeCollectionState episode_collection_state = 2; + optional cosmos_util.proto.EpisodeSyncState episode_offline_state = 3; + optional cosmos_util.proto.EpisodePlayState episode_play_state = 4; + optional string link = 5; +} + +message Show { + optional cosmos_util.proto.ShowMetadata show_metadata = 1; + optional cosmos_util.proto.ShowCollectionState show_collection_state = 2; + optional cosmos_util.proto.ShowPlayState show_play_state = 3; + optional string link = 4; +} + +message Track { + optional cosmos_util.proto.TrackMetadata track_metadata = 1; + optional cosmos_util.proto.TrackSyncState track_offline_state = 2; + optional cosmos_util.proto.TrackPlayState track_play_state = 3; + optional cosmos_util.proto.TrackCollectionState track_collection_state = 4; + optional string link = 5; +} + +message Response { + repeated Show show = 1; + repeated Episode episode = 2; + repeated Album album = 3; + repeated Artist artist = 4; + repeated Track track = 5; +} diff --git a/protocol/proto/cosmos_get_album_list_request.proto b/protocol/proto/cosmos_get_album_list_request.proto new file mode 100644 index 00000000..741e9f49 --- /dev/null +++ b/protocol/proto/cosmos_get_album_list_request.proto @@ -0,0 +1,37 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.collection_cosmos.album_list_request.proto; + +import "collection/album_collection_state.proto"; +import "sync/album_sync_state.proto"; +import "metadata/album_metadata.proto"; + +option optimize_for = CODE_SIZE; + +message Item { + optional string header_field = 1; + optional uint32 index = 2; + optional uint32 add_time = 3; + optional cosmos_util.proto.AlbumMetadata album_metadata = 4; + optional cosmos_util.proto.AlbumCollectionState album_collection_state = 5; + optional cosmos_util.proto.AlbumSyncState album_offline_state = 6; + optional string group_label = 7; +} + +message GroupHeader { + optional string header_field = 1; + optional uint32 index = 2; + optional uint32 length = 3; +} + +message Response { + repeated Item item = 1; + optional uint32 unfiltered_length = 2; + optional uint32 unranged_length = 3; + optional bool loading_contents = 4; + optional string offline = 5; + optional uint32 sync_progress = 6; + repeated GroupHeader group_index = 7; +} diff --git a/protocol/proto/cosmos_get_artist_list_request.proto b/protocol/proto/cosmos_get_artist_list_request.proto new file mode 100644 index 00000000..b8ccb662 --- /dev/null +++ b/protocol/proto/cosmos_get_artist_list_request.proto @@ -0,0 +1,37 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.collection_cosmos.artist_list_request.proto; + +import "collection/artist_collection_state.proto"; +import "sync/artist_sync_state.proto"; +import "metadata/artist_metadata.proto"; + +option optimize_for = CODE_SIZE; + +message Item { + optional string header_field = 1; + optional uint32 index = 2; + optional uint32 add_time = 3; + optional cosmos_util.proto.ArtistMetadata artist_metadata = 4; + optional cosmos_util.proto.ArtistCollectionState artist_collection_state = 5; + optional cosmos_util.proto.ArtistSyncState artist_offline_state = 6; + optional string group_label = 7; +} + +message GroupHeader { + optional string header_field = 1; + optional uint32 index = 2; + optional uint32 length = 3; +} + +message Response { + repeated Item item = 1; + optional uint32 unfiltered_length = 2; + optional uint32 unranged_length = 3; + optional bool loading_contents = 4; + optional string offline = 5; + optional uint32 sync_progress = 6; + repeated GroupHeader group_index = 7; +} diff --git a/protocol/proto/cosmos_get_episode_list_request.proto b/protocol/proto/cosmos_get_episode_list_request.proto new file mode 100644 index 00000000..8168fbfe --- /dev/null +++ b/protocol/proto/cosmos_get_episode_list_request.proto @@ -0,0 +1,27 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.collection_cosmos.episode_list_request.proto; + +import "collection/episode_collection_state.proto"; +import "played_state/episode_played_state.proto"; +import "sync/episode_sync_state.proto"; +import "metadata/episode_metadata.proto"; + +option optimize_for = CODE_SIZE; + +message Item { + optional string header = 1; + optional cosmos_util.proto.EpisodeMetadata episode_metadata = 2; + optional cosmos_util.proto.EpisodeCollectionState episode_collection_state = 3; + optional cosmos_util.proto.EpisodeSyncState episode_offline_state = 4; + optional cosmos_util.proto.EpisodePlayState episode_play_state = 5; +} + +message Response { + repeated Item item = 1; + optional uint32 unfiltered_length = 2; + optional uint32 unranged_length = 3; + optional bool loading_contents = 4; +} diff --git a/protocol/proto/cosmos_get_show_list_request.proto b/protocol/proto/cosmos_get_show_list_request.proto new file mode 100644 index 00000000..880f7cea --- /dev/null +++ b/protocol/proto/cosmos_get_show_list_request.proto @@ -0,0 +1,30 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.collection_cosmos.show_list_request.proto; + +import "collection/show_collection_state.proto"; +import "played_state/show_played_state.proto"; +import "metadata/show_metadata.proto"; + +option optimize_for = CODE_SIZE; + +message Item { + optional string header_field = 1; + optional cosmos_util.proto.ShowMetadata show_metadata = 2; + optional cosmos_util.proto.ShowCollectionState show_collection_state = 3; + optional cosmos_util.proto.ShowPlayState show_play_state = 4; + optional uint32 headerless_index = 5; + optional uint32 add_time = 6; + optional bool has_new_episodes = 7; + optional uint64 latest_published_episode_date = 8; +} + +message Response { + repeated Item item = 1; + optional uint32 num_offlined_episodes = 2; + optional uint32 unfiltered_length = 3; + optional uint32 unranged_length = 4; + optional bool loading_contents = 5; +} diff --git a/protocol/proto/cosmos_get_tags_info_request.proto b/protocol/proto/cosmos_get_tags_info_request.proto new file mode 100644 index 00000000..fe666025 --- /dev/null +++ b/protocol/proto/cosmos_get_tags_info_request.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.collection_cosmos.tags_info_request.proto; + +option optimize_for = CODE_SIZE; + +message Response { + bool is_synced = 1; +} diff --git a/protocol/proto/cosmos_get_track_list_metadata_request.proto b/protocol/proto/cosmos_get_track_list_metadata_request.proto new file mode 100644 index 00000000..8a02c962 --- /dev/null +++ b/protocol/proto/cosmos_get_track_list_metadata_request.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.collection_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message TrackListMetadata { + optional uint32 unfiltered_length = 1; + optional uint32 length = 2; + optional string offline = 3; + optional uint32 sync_progress = 4; +} diff --git a/protocol/proto/cosmos_get_track_list_request.proto b/protocol/proto/cosmos_get_track_list_request.proto new file mode 100644 index 00000000..c92320f7 --- /dev/null +++ b/protocol/proto/cosmos_get_track_list_request.proto @@ -0,0 +1,39 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.collection_cosmos.track_list_request.proto; + +import "collection/track_collection_state.proto"; +import "played_state/track_played_state.proto"; +import "sync/track_sync_state.proto"; +import "metadata/track_metadata.proto"; + +option optimize_for = CODE_SIZE; + +message Item { + optional string header_field = 1; + optional uint32 index = 2; + optional uint32 add_time = 3; + optional cosmos_util.proto.TrackMetadata track_metadata = 4; + optional cosmos_util.proto.TrackSyncState track_offline_state = 5; + optional cosmos_util.proto.TrackPlayState track_play_state = 6; + optional cosmos_util.proto.TrackCollectionState track_collection_state = 7; + optional string group_label = 8; +} + +message GroupHeader { + optional string header_field = 1; + optional uint32 index = 2; + optional uint32 length = 3; +} + +message Response { + repeated Item item = 1; + optional uint32 unfiltered_length = 2; + optional uint32 unranged_length = 3; + optional bool loading_contents = 4; + optional string offline = 5; + optional uint32 sync_progress = 6; + repeated GroupHeader group_index = 7; +} diff --git a/protocol/proto/cosmos_get_unplayed_episodes_request.proto b/protocol/proto/cosmos_get_unplayed_episodes_request.proto new file mode 100644 index 00000000..8957ae56 --- /dev/null +++ b/protocol/proto/cosmos_get_unplayed_episodes_request.proto @@ -0,0 +1,27 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.collection_cosmos.unplayed_request.proto; + +import "collection/episode_collection_state.proto"; +import "played_state/episode_played_state.proto"; +import "sync/episode_sync_state.proto"; +import "metadata/episode_metadata.proto"; + +option optimize_for = CODE_SIZE; + +message Item { + optional string header = 1; + optional cosmos_util.proto.EpisodeMetadata episode_metadata = 2; + optional cosmos_util.proto.EpisodeCollectionState episode_collection_state = 3; + optional cosmos_util.proto.EpisodeSyncState episode_offline_state = 4; + optional cosmos_util.proto.EpisodePlayState episode_play_state = 5; +} + +message Response { + repeated Item item = 1; + optional uint32 unfiltered_length = 2; + optional uint32 unranged_length = 3; + optional bool loading_contents = 4; +} diff --git a/protocol/proto/cuepoints.proto b/protocol/proto/cuepoints.proto new file mode 100644 index 00000000..16bfd6a9 --- /dev/null +++ b/protocol/proto/cuepoints.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.automix.proto; + +option optimize_for = CODE_SIZE; + +message Cuepoint { + int64 position_ms = 1; + float tempo_bpm = 2; + Origin origin = 3; +} + +message Cuepoints { + Cuepoint fade_in_cuepoint = 1; + Cuepoint fade_out_cuepoint = 2; +} + +enum Origin { + HUMAN = 0; + ML = 1; +} diff --git a/protocol/proto/decorate_request.proto b/protocol/proto/decorate_request.proto new file mode 100644 index 00000000..cad3f526 --- /dev/null +++ b/protocol/proto/decorate_request.proto @@ -0,0 +1,48 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.show_cosmos.decorate_request.proto; + +import "metadata/episode_metadata.proto"; +import "metadata/show_metadata.proto"; +import "show_access.proto"; +import "show_episode_state.proto"; +import "show_show_state.proto"; +import "podcast_segments.proto"; +import "podcast_virality.proto"; +import "podcastextensions.proto"; +import "podcast_poll.proto"; +import "podcast_qna.proto"; +import "transcripts.proto"; + +option optimize_for = CODE_SIZE; + +message Show { + optional cosmos_util.proto.ShowMetadata show_metadata = 1; + optional show_cosmos.proto.ShowCollectionState show_collection_state = 2; + optional show_cosmos.proto.ShowPlayState show_play_state = 3; + optional string link = 4; + optional podcast_paywalls.ShowAccess access_info = 5; +} + +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 string link = 5; + optional podcast_segments.PodcastSegments segments = 6; + optional podcast.extensions.PodcastHtmlDescription html_description = 7; + optional corex.transcripts.metadata.EpisodeTranscript transcripts = 9; + optional podcastvirality.v1.PodcastVirality virality = 10; + optional polls.PodcastPoll podcast_poll = 11; + optional qanda.PodcastQna podcast_qna = 12; + + reserved 8; +} + +message Response { + repeated Show show = 1; + repeated Episode episode = 2; +} diff --git a/protocol/proto/dependencies/session_control.proto b/protocol/proto/dependencies/session_control.proto new file mode 100644 index 00000000..f4e6d744 --- /dev/null +++ b/protocol/proto/dependencies/session_control.proto @@ -0,0 +1,121 @@ +// 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/devices.proto b/protocol/proto/devices.proto new file mode 100644 index 00000000..ebfadc1b --- /dev/null +++ b/protocol/proto/devices.proto @@ -0,0 +1,35 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.connectstate.devices; + +option java_package = "com.spotify.common.proto"; + +message DeviceAlias { + uint32 id = 1; + string display_name = 2; + bool is_group = 3; +} + +enum DeviceType { + UNKNOWN = 0; + COMPUTER = 1; + TABLET = 2; + SMARTPHONE = 3; + SPEAKER = 4; + TV = 5; + AVR = 6; + STB = 7; + AUDIO_DONGLE = 8; + GAME_CONSOLE = 9; + CAST_VIDEO = 10; + CAST_AUDIO = 11; + AUTOMOBILE = 12; + SMARTWATCH = 13; + CHROMEBOOK = 14; + UNKNOWN_SPOTIFY = 100; + CAR_THING = 101; + OBSERVER = 102; + HOME_THING = 103; +} diff --git a/protocol/proto/display_segments.proto b/protocol/proto/display_segments.proto new file mode 100644 index 00000000..eb3e02b3 --- /dev/null +++ b/protocol/proto/display_segments.proto @@ -0,0 +1,40 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.podcast_segments.display; + +import "podcast_segments.proto"; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "DisplaySegmentsProto"; +option java_package = "com.spotify.podcastsegments.display.proto"; + +message DisplaySegments { + repeated DisplaySegment display_segments = 1; + bool can_upsell = 2; + string album_mosaic_uri = 3; + repeated string artists = 4; + int32 duration_ms = 5; +} + +message DisplaySegment { + string uri = 1; + int32 absolute_start_ms = 2; + int32 absolute_stop_ms = 3; + + Source source = 4; + enum Source { + PLAYBACK = 0; + EMBEDDED = 1; + } + + SegmentType type = 5; + string title = 6; + string subtitle = 7; + string image_url = 8; + string action_url = 9; + bool is_abridged = 10; +} diff --git a/protocol/proto/entity_extension_data.proto b/protocol/proto/entity_extension_data.proto new file mode 100644 index 00000000..e26d735e --- /dev/null +++ b/protocol/proto/entity_extension_data.proto @@ -0,0 +1,39 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.extendedmetadata; + +import "google/protobuf/any.proto"; + +option cc_enable_arenas = true; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.extendedmetadata.proto"; + +message EntityExtensionDataHeader { + int32 status_code = 1; + string etag = 2; + string locale = 3; + int64 cache_ttl_in_seconds = 4; + int64 offline_ttl_in_seconds = 5; +} + +message EntityExtensionData { + EntityExtensionDataHeader header = 1; + string entity_uri = 2; + google.protobuf.Any extension_data = 3; +} + +message PlainListAssoc { + repeated string entity_uri = 1; +} + +message AssocHeader { + +} + +message Assoc { + AssocHeader header = 1; + PlainListAssoc plain_list = 2; +} diff --git a/protocol/proto/es_add_to_queue_request.proto b/protocol/proto/es_add_to_queue_request.proto new file mode 100644 index 00000000..34997731 --- /dev/null +++ b/protocol/proto/es_add_to_queue_request.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_context_track.proto"; +import "es_logging_params.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message AddToQueueRequest { + ContextTrack track = 1; + CommandOptions options = 2; + LoggingParams logging_params = 3; +} diff --git a/protocol/proto/es_command_options.proto b/protocol/proto/es_command_options.proto new file mode 100644 index 00000000..c261ca27 --- /dev/null +++ b/protocol/proto/es_command_options.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message CommandOptions { + bool override_restrictions = 1; + bool only_for_local_device = 2; + bool system_initiated = 3; +} diff --git a/protocol/proto/es_context.proto b/protocol/proto/es_context.proto new file mode 100644 index 00000000..05962fa7 --- /dev/null +++ b/protocol/proto/es_context.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_context_page.proto"; +import "es_restrictions.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message Context { + repeated ContextPage pages = 1; + map metadata = 2; + string uri = 3; + string url = 4; + bool is_loaded = 5; + Restrictions restrictions = 6; +} diff --git a/protocol/proto/es_context_page.proto b/protocol/proto/es_context_page.proto new file mode 100644 index 00000000..f4cc6930 --- /dev/null +++ b/protocol/proto/es_context_page.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_context_track.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message ContextPage { + repeated ContextTrack tracks = 1; + map metadata = 2; + string page_url = 3; + string next_page_url = 4; + bool is_loaded = 5; +} diff --git a/protocol/proto/es_context_player_error.proto b/protocol/proto/es_context_player_error.proto new file mode 100644 index 00000000..f332fe8a --- /dev/null +++ b/protocol/proto/es_context_player_error.proto @@ -0,0 +1,55 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message ContextPlayerError { + ErrorCode code = 1; + enum ErrorCode { + SUCCESS = 0; + PLAYBACK_STUCK = 1; + PLAYBACK_ERROR = 2; + LICENSE_CHANGE = 3; + PLAY_RESTRICTED = 4; + STOP_RESTRICTED = 5; + UPDATE_RESTRICTED = 6; + PAUSE_RESTRICTED = 7; + RESUME_RESTRICTED = 8; + SKIP_TO_PREV_RESTRICTED = 9; + SKIP_TO_NEXT_RESTRICTED = 10; + SKIP_TO_NON_EXISTENT_TRACK = 11; + SEEK_TO_RESTRICTED = 12; + TOGGLE_REPEAT_CONTEXT_RESTRICTED = 13; + TOGGLE_REPEAT_TRACK_RESTRICTED = 14; + SET_OPTIONS_RESTRICTED = 15; + TOGGLE_SHUFFLE_RESTRICTED = 16; + SET_QUEUE_RESTRICTED = 17; + INTERRUPT_PLAYBACK_RESTRICTED = 18; + ONE_TRACK_UNPLAYABLE = 19; + ONE_TRACK_UNPLAYABLE_AUTO_STOPPED = 20; + ALL_TRACKS_UNPLAYABLE_AUTO_STOPPED = 21; + SKIP_TO_NON_EXISTENT_TRACK_AUTO_STOPPED = 22; + QUEUE_REVISION_MISMATCH = 23; + VIDEO_PLAYBACK_ERROR = 24; + VIDEO_GEOGRAPHICALLY_RESTRICTED = 25; + VIDEO_UNSUPPORTED_PLATFORM_VERSION = 26; + VIDEO_UNSUPPORTED_CLIENT_VERSION = 27; + VIDEO_UNSUPPORTED_KEY_SYSTEM = 28; + VIDEO_MANIFEST_DELETED = 29; + VIDEO_COUNTRY_RESTRICTED = 30; + VIDEO_UNAVAILABLE = 31; + VIDEO_CATALOGUE_RESTRICTED = 32; + INVALID = 33; + TIMEOUT = 34; + PLAYBACK_REPORTING_ERROR = 35; + UNKNOWN = 36; + } + + string message = 2; + map data = 3; +} diff --git a/protocol/proto/es_context_player_options.proto b/protocol/proto/es_context_player_options.proto new file mode 100644 index 00000000..372b53c0 --- /dev/null +++ b/protocol/proto/es_context_player_options.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_optional.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message ContextPlayerOptions { + bool shuffling_context = 1; + bool repeating_context = 2; + bool repeating_track = 3; +} + +message ContextPlayerOptionOverrides { + OptionalBoolean shuffling_context = 1; + OptionalBoolean repeating_context = 2; + OptionalBoolean repeating_track = 3; +} diff --git a/protocol/proto/es_context_player_state.proto b/protocol/proto/es_context_player_state.proto new file mode 100644 index 00000000..f1626572 --- /dev/null +++ b/protocol/proto/es_context_player_state.proto @@ -0,0 +1,82 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_restrictions.proto"; +import "es_play_origin.proto"; +import "es_optional.proto"; +import "es_provided_track.proto"; +import "es_context_player_options.proto"; +import "es_prepare_play_options.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message ContextIndex { + uint64 page = 1; + uint64 track = 2; +} + +message PlaybackQuality { + BitrateLevel bitrate_level = 1; + enum BitrateLevel { + UNKNOWN = 0; + LOW = 1; + NORMAL = 2; + HIGH = 3; + VERY_HIGH = 4; + HIFI = 5; + } + + BitrateStrategy strategy = 2; + enum BitrateStrategy { + UNKNOWN_STRATEGY = 0; + BEST_MATCHING = 1; + BACKEND_ADVISED = 2; + OFFLINED_FILE = 3; + CACHED_FILE = 4; + LOCAL_FILE = 5; + } + + BitrateLevel target_bitrate_level = 3; + bool target_bitrate_available = 4; + + HiFiStatus hifi_status = 5; + enum HiFiStatus { + NONE = 0; + OFF = 1; + ON = 2; + } +} + +message ContextPlayerState { + uint64 timestamp = 1; + string context_uri = 2; + string context_url = 3; + Restrictions context_restrictions = 4; + PlayOrigin play_origin = 5; + ContextIndex index = 6; + ProvidedTrack track = 7; + bytes playback_id = 8; + PlaybackQuality playback_quality = 9; + OptionalDouble playback_speed = 10; + OptionalInt64 position_as_of_timestamp = 11; + OptionalInt64 duration = 12; + bool is_playing = 13; + bool is_paused = 14; + bool is_buffering = 15; + bool is_system_initiated = 16; + ContextPlayerOptions options = 17; + Restrictions restrictions = 18; + repeated string suppressions = 19; + repeated ProvidedTrack prev_tracks = 20; + repeated ProvidedTrack next_tracks = 21; + map context_metadata = 22; + map page_metadata = 23; + string session_id = 24; + uint64 queue_revision = 25; + PreparePlayOptions.AudioStream audio_stream = 26; +} diff --git a/protocol/proto/es_context_track.proto b/protocol/proto/es_context_track.proto new file mode 100644 index 00000000..cdcbd7c2 --- /dev/null +++ b/protocol/proto/es_context_track.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message ContextTrack { + string uri = 1; + string uid = 2; + map metadata = 3; +} diff --git a/protocol/proto/es_delete_session.proto b/protocol/proto/es_delete_session.proto new file mode 100644 index 00000000..e45893c4 --- /dev/null +++ b/protocol/proto/es_delete_session.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message DeleteSessionRequest { + string session_id = 1; +} + +message DeleteSessionResponse { + +} diff --git a/protocol/proto/es_get_error_request.proto b/protocol/proto/es_get_error_request.proto new file mode 100644 index 00000000..3119beaa --- /dev/null +++ b/protocol/proto/es_get_error_request.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message GetErrorRequest { + +} diff --git a/protocol/proto/es_get_play_history.proto b/protocol/proto/es_get_play_history.proto new file mode 100644 index 00000000..08bb053c --- /dev/null +++ b/protocol/proto/es_get_play_history.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_context_track.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message GetPlayHistoryRequest { + +} + +message GetPlayHistoryResponse { + repeated ContextTrack tracks = 1; +} diff --git a/protocol/proto/es_get_position_state.proto b/protocol/proto/es_get_position_state.proto new file mode 100644 index 00000000..6147f0c5 --- /dev/null +++ b/protocol/proto/es_get_position_state.proto @@ -0,0 +1,25 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message GetPositionStateRequest { + +} + +message GetPositionStateResponse { + Error error = 1; + enum Error { + OK = 0; + NOT_FOUND = 1; + } + + uint64 timestamp = 2; + uint64 position = 3; + double playback_speed = 4; +} diff --git a/protocol/proto/es_get_queue_request.proto b/protocol/proto/es_get_queue_request.proto new file mode 100644 index 00000000..68b6830a --- /dev/null +++ b/protocol/proto/es_get_queue_request.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message GetQueueRequest { + +} diff --git a/protocol/proto/es_get_state_request.proto b/protocol/proto/es_get_state_request.proto new file mode 100644 index 00000000..d8cd5335 --- /dev/null +++ b/protocol/proto/es_get_state_request.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_optional.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message GetStateRequest { + OptionalInt64 prev_tracks_cap = 1; + OptionalInt64 next_tracks_cap = 2; +} diff --git a/protocol/proto/es_logging_params.proto b/protocol/proto/es_logging_params.proto new file mode 100644 index 00000000..c508cba2 --- /dev/null +++ b/protocol/proto/es_logging_params.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_optional.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message LoggingParams { + OptionalInt64 command_initiated_time = 1; + OptionalInt64 command_received_time = 2; + repeated string page_instance_ids = 3; + repeated string interaction_ids = 4; +} diff --git a/protocol/proto/es_optional.proto b/protocol/proto/es_optional.proto new file mode 100644 index 00000000..2ca0b01f --- /dev/null +++ b/protocol/proto/es_optional.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message OptionalInt64 { + int64 value = 1; +} + +message OptionalDouble { + double value = 1; +} + +message OptionalBoolean { + bool value = 1; +} diff --git a/protocol/proto/es_pause.proto b/protocol/proto/es_pause.proto new file mode 100644 index 00000000..56378fb5 --- /dev/null +++ b/protocol/proto/es_pause.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message PauseRequest { + CommandOptions options = 1; + LoggingParams logging_params = 2; +} diff --git a/protocol/proto/es_play.proto b/protocol/proto/es_play.proto new file mode 100644 index 00000000..34dca48a --- /dev/null +++ b/protocol/proto/es_play.proto @@ -0,0 +1,28 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; +import "es_play_options.proto"; +import "es_prepare_play.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message PlayRequest { + PreparePlayRequest prepare_play_request = 1; + PlayOptions play_options = 2; + CommandOptions options = 3; + LoggingParams logging_params = 4; +} + +message PlayPreparedRequest { + string session_id = 1; + PlayOptions play_options = 2; + CommandOptions options = 3; + LoggingParams logging_params = 4; +} diff --git a/protocol/proto/es_play_options.proto b/protocol/proto/es_play_options.proto new file mode 100644 index 00000000..f068921b --- /dev/null +++ b/protocol/proto/es_play_options.proto @@ -0,0 +1,32 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message PlayOptions { + Reason reason = 1; + enum Reason { + INTERACTIVE = 0; + REMOTE_TRANSFER = 1; + LICENSE_CHANGE = 2; + } + + Operation operation = 2; + enum Operation { + REPLACE = 0; + ENQUEUE = 1; + PUSH = 2; + } + + Trigger trigger = 3; + enum Trigger { + IMMEDIATELY = 0; + ADVANCED_PAST_TRACK = 1; + ADVANCED_PAST_CONTEXT = 2; + } +} diff --git a/protocol/proto/es_play_origin.proto b/protocol/proto/es_play_origin.proto new file mode 100644 index 00000000..62cff8b7 --- /dev/null +++ b/protocol/proto/es_play_origin.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message PlayOrigin { + string feature_identifier = 1; + string feature_version = 2; + string view_uri = 3; + string external_referrer = 4; + string referrer_identifier = 5; + string device_identifier = 6; + repeated string feature_classes = 7; +} diff --git a/protocol/proto/es_prepare_play.proto b/protocol/proto/es_prepare_play.proto new file mode 100644 index 00000000..6662eb65 --- /dev/null +++ b/protocol/proto/es_prepare_play.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_context.proto"; +import "es_play_origin.proto"; +import "es_prepare_play_options.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message PreparePlayRequest { + Context context = 1; + PreparePlayOptions options = 2; + PlayOrigin play_origin = 3; +} diff --git a/protocol/proto/es_prepare_play_options.proto b/protocol/proto/es_prepare_play_options.proto new file mode 100644 index 00000000..b4a4449c --- /dev/null +++ b/protocol/proto/es_prepare_play_options.proto @@ -0,0 +1,40 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_context_player_options.proto"; +import "es_optional.proto"; +import "es_skip_to_track.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message PreparePlayOptions { + bytes playback_id = 1; + bool always_play_something = 2; + SkipToTrack skip_to = 3; + OptionalInt64 seek_to = 4; + bool initially_paused = 5; + bool system_initiated = 6; + ContextPlayerOptionOverrides player_options_override = 7; + repeated string suppressions = 8; + + PrefetchLevel prefetch_level = 9; + enum PrefetchLevel { + NONE = 0; + MEDIA = 1; + } + + AudioStream audio_stream = 10; + enum AudioStream { + DEFAULT = 0; + ALARM = 1; + } + + string session_id = 11; + string license = 12; + map configuration_override = 13; +} diff --git a/protocol/proto/es_provided_track.proto b/protocol/proto/es_provided_track.proto new file mode 100644 index 00000000..6dcffc0d --- /dev/null +++ b/protocol/proto/es_provided_track.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_context_track.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message ProvidedTrack { + ContextTrack context_track = 1; + repeated string removed = 2; + repeated string blocked = 3; + string provider = 4; +} diff --git a/protocol/proto/es_queue.proto b/protocol/proto/es_queue.proto new file mode 100644 index 00000000..625b184d --- /dev/null +++ b/protocol/proto/es_queue.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_provided_track.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message Queue { + uint64 queue_revision = 1; + ProvidedTrack track = 2; + repeated ProvidedTrack next_tracks = 3; + repeated ProvidedTrack prev_tracks = 4; +} diff --git a/protocol/proto/es_response_with_reasons.proto b/protocol/proto/es_response_with_reasons.proto new file mode 100644 index 00000000..6570a177 --- /dev/null +++ b/protocol/proto/es_response_with_reasons.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message ResponseWithReasons { + Error error = 1; + enum Error { + OK = 0; + FORBIDDEN = 1; + NOT_FOUND = 2; + CONFLICT = 3; + } + + string reasons = 2; +} diff --git a/protocol/proto/es_restrictions.proto b/protocol/proto/es_restrictions.proto new file mode 100644 index 00000000..3a5c3a0a --- /dev/null +++ b/protocol/proto/es_restrictions.proto @@ -0,0 +1,33 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message Restrictions { + repeated string disallow_pausing_reasons = 1; + repeated string disallow_resuming_reasons = 2; + repeated string disallow_seeking_reasons = 3; + repeated string disallow_peeking_prev_reasons = 4; + repeated string disallow_peeking_next_reasons = 5; + repeated string disallow_skipping_prev_reasons = 6; + repeated string disallow_skipping_next_reasons = 7; + repeated string disallow_toggling_repeat_context_reasons = 8; + repeated string disallow_toggling_repeat_track_reasons = 9; + repeated string disallow_toggling_shuffle_reasons = 10; + repeated string disallow_set_queue_reasons = 11; + repeated string disallow_interrupting_playback_reasons = 12; + repeated string disallow_transferring_playback_reasons = 13; + repeated string disallow_remote_control_reasons = 14; + repeated string disallow_inserting_into_next_tracks_reasons = 15; + repeated string disallow_inserting_into_context_tracks_reasons = 16; + repeated string disallow_reordering_in_next_tracks_reasons = 17; + repeated string disallow_reordering_in_context_tracks_reasons = 18; + repeated string disallow_removing_from_next_tracks_reasons = 19; + repeated string disallow_removing_from_context_tracks_reasons = 20; + repeated string disallow_updating_context_reasons = 21; +} diff --git a/protocol/proto/es_resume.proto b/protocol/proto/es_resume.proto new file mode 100644 index 00000000..1af5980f --- /dev/null +++ b/protocol/proto/es_resume.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message ResumeRequest { + CommandOptions options = 1; + LoggingParams logging_params = 2; +} diff --git a/protocol/proto/es_seek_to.proto b/protocol/proto/es_seek_to.proto new file mode 100644 index 00000000..0ef8aa4b --- /dev/null +++ b/protocol/proto/es_seek_to.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message SeekToRequest { + CommandOptions options = 1; + LoggingParams logging_params = 2; + int64 position = 3; +} diff --git a/protocol/proto/es_session_response.proto b/protocol/proto/es_session_response.proto new file mode 100644 index 00000000..692ae30f --- /dev/null +++ b/protocol/proto/es_session_response.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message SessionResponse { + string session_id = 1; +} diff --git a/protocol/proto/es_set_options.proto b/protocol/proto/es_set_options.proto new file mode 100644 index 00000000..33faf5f8 --- /dev/null +++ b/protocol/proto/es_set_options.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; +import "es_optional.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message SetOptionsRequest { + OptionalBoolean repeating_track = 1; + OptionalBoolean repeating_context = 2; + OptionalBoolean shuffling_context = 3; + CommandOptions options = 4; + LoggingParams logging_params = 5; +} diff --git a/protocol/proto/es_set_queue_request.proto b/protocol/proto/es_set_queue_request.proto new file mode 100644 index 00000000..83715232 --- /dev/null +++ b/protocol/proto/es_set_queue_request.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_provided_track.proto"; +import "es_logging_params.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message SetQueueRequest { + repeated ProvidedTrack next_tracks = 1; + repeated ProvidedTrack prev_tracks = 2; + uint64 queue_revision = 3; + CommandOptions options = 4; + LoggingParams logging_params = 5; +} diff --git a/protocol/proto/es_set_repeating_context.proto b/protocol/proto/es_set_repeating_context.proto new file mode 100644 index 00000000..25667c81 --- /dev/null +++ b/protocol/proto/es_set_repeating_context.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message SetRepeatingContextRequest { + bool repeating_context = 1; + CommandOptions options = 2; + LoggingParams logging_params = 3; +} diff --git a/protocol/proto/es_set_repeating_track.proto b/protocol/proto/es_set_repeating_track.proto new file mode 100644 index 00000000..01ae3b56 --- /dev/null +++ b/protocol/proto/es_set_repeating_track.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message SetRepeatingTrackRequest { + bool repeating_track = 1; + CommandOptions options = 2; + LoggingParams logging_params = 3; +} diff --git a/protocol/proto/es_set_shuffling_context.proto b/protocol/proto/es_set_shuffling_context.proto new file mode 100644 index 00000000..6eb779e6 --- /dev/null +++ b/protocol/proto/es_set_shuffling_context.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message SetShufflingContextRequest { + bool shuffling_context = 1; + CommandOptions options = 2; + LoggingParams logging_params = 3; +} diff --git a/protocol/proto/es_skip_next.proto b/protocol/proto/es_skip_next.proto new file mode 100644 index 00000000..d6b0dc83 --- /dev/null +++ b/protocol/proto/es_skip_next.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; +import "es_context_track.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message SkipNextRequest { + CommandOptions options = 1; + LoggingParams logging_params = 2; + ContextTrack track = 3; +} diff --git a/protocol/proto/es_skip_prev.proto b/protocol/proto/es_skip_prev.proto new file mode 100644 index 00000000..2a6b9a71 --- /dev/null +++ b/protocol/proto/es_skip_prev.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; +import "es_context_track.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message SkipPrevRequest { + CommandOptions options = 1; + bool allow_seeking = 2; + LoggingParams logging_params = 3; + ContextTrack track = 4; +} diff --git a/protocol/proto/es_skip_to_track.proto b/protocol/proto/es_skip_to_track.proto new file mode 100644 index 00000000..ecf0d03f --- /dev/null +++ b/protocol/proto/es_skip_to_track.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_optional.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message SkipToTrack { + string page_url = 1; + OptionalInt64 page_index = 2; + string track_uid = 3; + string track_uri = 4; + OptionalInt64 track_index = 5; +} diff --git a/protocol/proto/es_stop.proto b/protocol/proto/es_stop.proto new file mode 100644 index 00000000..068490e0 --- /dev/null +++ b/protocol/proto/es_stop.proto @@ -0,0 +1,25 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_command_options.proto"; +import "es_logging_params.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message StopRequest { + CommandOptions options = 1; + + Reason reason = 2; + enum Reason { + INTERACTIVE = 0; + REMOTE_TRANSFER = 1; + SHUTDOWN = 2; + } + + LoggingParams logging_params = 3; +} diff --git a/protocol/proto/es_update.proto b/protocol/proto/es_update.proto new file mode 100644 index 00000000..90734b5d --- /dev/null +++ b/protocol/proto/es_update.proto @@ -0,0 +1,38 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +import "es_context.proto"; +import "es_context_page.proto"; +import "es_context_track.proto"; +import "es_logging_params.proto"; + +option objc_class_prefix = "ESP"; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.player.esperanto.proto"; + +message UpdateContextRequest { + string session_id = 1; + Context context = 2; + LoggingParams logging_params = 3; +} + +message UpdateContextPageRequest { + string session_id = 1; + ContextPage context_page = 2; + LoggingParams logging_params = 3; +} + +message UpdateContextTrackRequest { + string session_id = 1; + ContextTrack context_track = 2; + LoggingParams logging_params = 3; +} + +message UpdateViewUriRequest { + string session_id = 1; + string view_uri = 2; + LoggingParams logging_params = 3; +} diff --git a/protocol/proto/event_entity.proto b/protocol/proto/event_entity.proto new file mode 100644 index 00000000..28ec0b5a --- /dev/null +++ b/protocol/proto/event_entity.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message EventEntity { + int32 file_format_version = 1; + string event_name = 2; + bytes sequence_id = 3; + int64 sequence_number = 4; + bytes payload = 5; + string owner = 6; + bool authenticated = 7; + int64 record_id = 8; +} diff --git a/protocol/proto/explicit_content_pubsub.proto b/protocol/proto/explicit_content_pubsub.proto new file mode 100644 index 00000000..1bb45f91 --- /dev/null +++ b/protocol/proto/explicit_content_pubsub.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.explicit_content.proto; + +option optimize_for = CODE_SIZE; + +message KeyValuePair { + required string key = 1; + required string value = 2; +} + +message UserAttributesUpdate { + repeated KeyValuePair pairs = 1; +} diff --git a/protocol/proto/extended_metadata.proto b/protocol/proto/extended_metadata.proto new file mode 100644 index 00000000..2e38d28d --- /dev/null +++ b/protocol/proto/extended_metadata.proto @@ -0,0 +1,62 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.extendedmetadata; + +import "extension_kind.proto"; +import "entity_extension_data.proto"; + +option cc_enable_arenas = true; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.extendedmetadata.proto"; + +message ExtensionQuery { + ExtensionKind extension_kind = 1; + string etag = 2; +} + +message EntityRequest { + string entity_uri = 1; + repeated ExtensionQuery query = 2; +} + +message BatchedEntityRequestHeader { + string country = 1; + string catalogue = 2; + bytes task_id = 3; +} + +message BatchedEntityRequest { + BatchedEntityRequestHeader header = 1; + repeated EntityRequest entity_request = 2; +} + +message EntityExtensionDataArrayHeader { + int32 provider_error_status = 1; + int64 cache_ttl_in_seconds = 2; + int64 offline_ttl_in_seconds = 3; + ExtensionType extension_type = 4; +} + +message EntityExtensionDataArray { + EntityExtensionDataArrayHeader header = 1; + ExtensionKind extension_kind = 2; + repeated EntityExtensionData extension_data = 3; +} + +message BatchedExtensionResponseHeader { + +} + +message BatchedExtensionResponse { + BatchedExtensionResponseHeader header = 1; + repeated EntityExtensionDataArray extended_metadata = 2; +} + +enum ExtensionType { + UNKNOWN = 0; + GENERIC = 1; + ASSOC = 2; +} diff --git a/protocol/proto/extension_descriptor_type.proto b/protocol/proto/extension_descriptor_type.proto new file mode 100644 index 00000000..a2009d68 --- /dev/null +++ b/protocol/proto/extension_descriptor_type.proto @@ -0,0 +1,29 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.descriptorextension; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.descriptorextension.proto"; + +message ExtensionDescriptor { + string text = 1; + float weight = 2; + repeated ExtensionDescriptorType types = 3; +} + +message ExtensionDescriptorData { + repeated ExtensionDescriptor descriptors = 1; +} + +enum ExtensionDescriptorType { + UNKNOWN = 0; + GENRE = 1; + MOOD = 2; + ACTIVITY = 3; + INSTRUMENT = 4; + TIME = 5; + ERA = 6; +} diff --git a/protocol/proto/extension_kind.proto b/protocol/proto/extension_kind.proto new file mode 100644 index 00000000..97768b67 --- /dev/null +++ b/protocol/proto/extension_kind.proto @@ -0,0 +1,46 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.extendedmetadata; + +option cc_enable_arenas = true; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.extendedmetadata.proto"; + +enum ExtensionKind { + UNKNOWN_EXTENSION = 0; + CANVAZ = 1; + STORYLINES = 2; + PODCAST_TOPICS = 3; + PODCAST_SEGMENTS = 4; + AUDIO_FILES = 5; + TRACK_DESCRIPTOR = 6; + ARTIST_V4 = 8; + ALBUM_V4 = 9; + TRACK_V4 = 10; + SHOW_V4 = 11; + EPISODE_V4 = 12; + PODCAST_HTML_DESCRIPTION = 13; + PODCAST_QUOTES = 14; + USER_PROFILE = 15; + CANVAS_V1 = 16; + SHOW_V4_BASE = 17; + SHOW_V4_EPISODES_ASSOC = 18; + TRACK_DESCRIPTOR_SIGNATURES = 19; + PODCAST_AD_SEGMENTS = 20; + EPISODE_TRANSCRIPTS = 21; + PODCAST_SUBSCRIPTIONS = 22; + EXTRACTED_COLOR = 23; + PODCAST_VIRALITY = 24; + IMAGE_SPARKLES_HACK = 25; + PODCAST_POPULARITY_HACK = 26; + AUTOMIX_MODE = 27; + CUEPOINTS = 28; + PODCAST_POLL = 29; + EPISODE_ACCESS = 30; + SHOW_ACCESS = 31; + PODCAST_QNA = 32; + CLIPS = 33; +} diff --git a/protocol/proto/extracted_colors.proto b/protocol/proto/extracted_colors.proto new file mode 100644 index 00000000..999a27ea --- /dev/null +++ b/protocol/proto/extracted_colors.proto @@ -0,0 +1,24 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.context_track_color; + +message ColorResult { + Color color_raw = 1; + Color color_light = 2; + Color color_dark = 3; + Status status = 5; +} + +message Color { + int32 rgb = 1; + bool is_fallback = 2; +} + +enum Status { + OK = 0; + IN_PROGRESS = 1; + INVALID_URL = 2; + INTERNAL = 3; +} diff --git a/protocol/proto/facebook-publish.proto b/protocol/proto/facebook-publish.proto deleted file mode 100644 index 4edef249..00000000 --- a/protocol/proto/facebook-publish.proto +++ /dev/null @@ -1,51 +0,0 @@ -syntax = "proto2"; - -message EventReply { - optional int32 queued = 0x1; - optional RetryInfo retry = 0x2; -} - -message RetryInfo { - optional int32 retry_delay = 0x1; - optional int32 max_retry = 0x2; -} - -message Id { - optional string uri = 0x1; - optional int64 start_time = 0x2; -} - -message Start { - optional int32 length = 0x1; - optional string context_uri = 0x2; - optional int64 end_time = 0x3; -} - -message Seek { - optional int64 end_time = 0x1; -} - -message Pause { - optional int32 seconds_played = 0x1; - optional int64 end_time = 0x2; -} - -message Resume { - optional int32 seconds_played = 0x1; - optional int64 end_time = 0x2; -} - -message End { - optional int32 seconds_played = 0x1; - optional int64 end_time = 0x2; -} - -message Event { - optional Id id = 0x1; - optional Start start = 0x2; - optional Seek seek = 0x3; - optional Pause pause = 0x4; - optional Resume resume = 0x5; - optional End end = 0x6; -} - diff --git a/protocol/proto/facebook.proto b/protocol/proto/facebook.proto deleted file mode 100644 index 8227c5a1..00000000 --- a/protocol/proto/facebook.proto +++ /dev/null @@ -1,183 +0,0 @@ -syntax = "proto2"; - -message Credential { - optional string facebook_uid = 0x1; - optional string access_token = 0x2; -} - -message EnableRequest { - optional Credential credential = 0x1; -} - -message EnableReply { - optional Credential credential = 0x1; -} - -message DisableRequest { - optional Credential credential = 0x1; -} - -message RevokeRequest { - optional Credential credential = 0x1; -} - -message InspectCredentialRequest { - optional Credential credential = 0x1; -} - -message InspectCredentialReply { - optional Credential alternative_credential = 0x1; - optional bool app_user = 0x2; - optional bool permanent_error = 0x3; - optional bool transient_error = 0x4; -} - -message UserState { - optional Credential credential = 0x1; -} - -message UpdateUserStateRequest { - optional Credential credential = 0x1; -} - -message OpenGraphError { - repeated string permanent = 0x1; - repeated string invalid_token = 0x2; - repeated string retries = 0x3; -} - -message OpenGraphScrobble { - optional int32 create_delay = 0x1; -} - -message OpenGraphConfig { - optional OpenGraphError error = 0x1; - optional OpenGraphScrobble scrobble = 0x2; -} - -message AuthConfig { - optional string url = 0x1; - repeated string permissions = 0x2; - repeated string blacklist = 0x3; - repeated string whitelist = 0x4; - repeated string cancel = 0x5; -} - -message ConfigReply { - optional string domain = 0x1; - optional string app_id = 0x2; - optional string app_namespace = 0x3; - optional AuthConfig auth = 0x4; - optional OpenGraphConfig og = 0x5; -} - -message UserFields { - optional bool app_user = 0x1; - optional bool display_name = 0x2; - optional bool first_name = 0x3; - optional bool middle_name = 0x4; - optional bool last_name = 0x5; - optional bool picture_large = 0x6; - optional bool picture_square = 0x7; - optional bool gender = 0x8; - optional bool email = 0x9; -} - -message UserOptions { - optional bool cache_is_king = 0x1; -} - -message UserRequest { - optional UserOptions options = 0x1; - optional UserFields fields = 0x2; -} - -message User { - optional string spotify_username = 0x1; - optional string facebook_uid = 0x2; - optional bool app_user = 0x3; - optional string display_name = 0x4; - optional string first_name = 0x5; - optional string middle_name = 0x6; - optional string last_name = 0x7; - optional string picture_large = 0x8; - optional string picture_square = 0x9; - optional string gender = 0xa; - optional string email = 0xb; -} - -message FriendsFields { - optional bool app_user = 0x1; - optional bool display_name = 0x2; - optional bool picture_large = 0x6; -} - -message FriendsOptions { - optional int32 limit = 0x1; - optional int32 offset = 0x2; - optional bool cache_is_king = 0x3; - optional bool app_friends = 0x4; - optional bool non_app_friends = 0x5; -} - -message FriendsRequest { - optional FriendsOptions options = 0x1; - optional FriendsFields fields = 0x2; -} - -message FriendsReply { - repeated User friends = 0x1; - optional bool more = 0x2; -} - -message ShareRequest { - optional Credential credential = 0x1; - optional string uri = 0x2; - optional string message_text = 0x3; -} - -message ShareReply { - optional string post_id = 0x1; -} - -message InboxRequest { - optional Credential credential = 0x1; - repeated string facebook_uids = 0x3; - optional string message_text = 0x4; - optional string message_link = 0x5; -} - -message InboxReply { - optional string message_id = 0x1; - optional string thread_id = 0x2; -} - -message PermissionsOptions { - optional bool cache_is_king = 0x1; -} - -message PermissionsRequest { - optional Credential credential = 0x1; - optional PermissionsOptions options = 0x2; -} - -message PermissionsReply { - repeated string permissions = 0x1; -} - -message GrantPermissionsRequest { - optional Credential credential = 0x1; - repeated string permissions = 0x2; -} - -message GrantPermissionsReply { - repeated string granted = 0x1; - repeated string failed = 0x2; -} - -message TransferRequest { - optional Credential credential = 0x1; - optional string source_username = 0x2; - optional string target_username = 0x3; -} - diff --git a/protocol/proto/format.proto b/protocol/proto/format.proto new file mode 100644 index 00000000..3a75b4df --- /dev/null +++ b/protocol/proto/format.proto @@ -0,0 +1,30 @@ +// 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 Format { + FORMAT_UNKNOWN = 0; + FORMAT_OGG_VORBIS_96 = 1; + FORMAT_OGG_VORBIS_160 = 2; + FORMAT_OGG_VORBIS_320 = 3; + FORMAT_MP3_256 = 4; + FORMAT_MP3_320 = 5; + FORMAT_MP3_160 = 6; + FORMAT_MP3_96 = 7; + FORMAT_MP3_160_ENCRYPTED = 8; + FORMAT_AAC_24 = 9; + FORMAT_AAC_48 = 10; + FORMAT_MP4_128 = 11; + FORMAT_MP4_128_DUAL = 12; + FORMAT_MP4_128_CBCS = 13; + FORMAT_MP4_256 = 14; + FORMAT_MP4_256_DUAL = 15; + FORMAT_MP4_256_CBCS = 16; + FORMAT_FLAC_FLAC = 17; + FORMAT_MP4_FLAC = 18; +} diff --git a/protocol/proto/frecency.proto b/protocol/proto/frecency.proto new file mode 100644 index 00000000..89c6c7f6 --- /dev/null +++ b/protocol/proto/frecency.proto @@ -0,0 +1,27 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.frecency.v1; + +import "google/protobuf/timestamp.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "FrecencyProto"; +option java_package = "com.spotify.frecency.v1"; + +message FrecencyResponse { + repeated PlayContext play_contexts = 1; +} + +message PlayContext { + string uri = 1; + Frecency frecency = 2; +} + +message Frecency { + double ln_frecency = 1; + int32 event_count = 2; + google.protobuf.Timestamp last_event_time = 3; +} diff --git a/protocol/proto/frecency_storage.proto b/protocol/proto/frecency_storage.proto new file mode 100644 index 00000000..9e32269f --- /dev/null +++ b/protocol/proto/frecency_storage.proto @@ -0,0 +1,25 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.frecency.proto.storage; + +option cc_enable_arenas = true; +option optimize_for = CODE_SIZE; + +message Frecency { + optional double ln_frecency = 1; + optional uint64 event_count = 2; + optional uint32 event_kind = 3; + optional uint64 last_event_time = 4; +} + +message ContextFrecencyInfo { + optional string context_uri = 1; + repeated Frecency context_frecencies = 2; +} + +message ContextFrecencyFile { + repeated ContextFrecencyInfo contexts = 1; + optional uint64 frecency_version = 2; +} diff --git a/protocol/proto/gabito.proto b/protocol/proto/gabito.proto new file mode 100644 index 00000000..b47f4fdd --- /dev/null +++ b/protocol/proto/gabito.proto @@ -0,0 +1,36 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message EventEnvelope { + string event_name = 2; + + repeated EventFragment event_fragment = 3; + message EventFragment { + string name = 1; + bytes data = 2; + } + + bytes sequence_id = 4; + int64 sequence_number = 5; + + reserved 1; +} + +message PublishEventsRequest { + repeated EventEnvelope event = 1; + bool suppress_persist = 2; +} + +message PublishEventsResponse { + repeated EventError error = 1; + message EventError { + int32 index = 1; + bool transient = 2; + int32 reason = 3; + } +} diff --git a/protocol/proto/global_node.proto b/protocol/proto/global_node.proto new file mode 100644 index 00000000..cd6f1b6c --- /dev/null +++ b/protocol/proto/global_node.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +import "context_player_options.proto"; +import "player_license.proto"; + +option optimize_for = CODE_SIZE; + +message GlobalNode { + optional ContextPlayerOptions options = 1; + optional PlayerLicense license = 2; + map configuration = 3; +} diff --git a/protocol/proto/google/protobuf/any.proto b/protocol/proto/google/protobuf/any.proto new file mode 100644 index 00000000..bb7f136c --- /dev/null +++ b/protocol/proto/google/protobuf/any.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option objc_class_prefix = "GPB"; +option go_package = "google.golang.org/protobuf/types/known/anypb"; +option java_multiple_files = true; +option java_outer_classname = "AnyProto"; +option java_package = "com.google.protobuf"; + +message Any { + string type_url = 1; + bytes value = 2; +} diff --git a/protocol/proto/google/protobuf/descriptor.proto b/protocol/proto/google/protobuf/descriptor.proto new file mode 100644 index 00000000..7f91c408 --- /dev/null +++ b/protocol/proto/google/protobuf/descriptor.proto @@ -0,0 +1,301 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.Reflection"; +option objc_class_prefix = "GPB"; +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/descriptorpb"; +option optimize_for = SPEED; +option java_outer_classname = "DescriptorProtos"; +option java_package = "com.google.protobuf"; + +message FileDescriptorSet { + repeated FileDescriptorProto file = 1; +} + +message FileDescriptorProto { + optional string name = 1; + optional string package = 2; + repeated string dependency = 3; + repeated int32 public_dependency = 10; + repeated int32 weak_dependency = 11; + repeated DescriptorProto message_type = 4; + repeated EnumDescriptorProto enum_type = 5; + repeated ServiceDescriptorProto service = 6; + repeated FieldDescriptorProto extension = 7; + optional FileOptions options = 8; + optional SourceCodeInfo source_code_info = 9; + optional string syntax = 12; +} + +message DescriptorProto { + optional string name = 1; + repeated FieldDescriptorProto field = 2; + repeated FieldDescriptorProto extension = 6; + repeated DescriptorProto nested_type = 3; + repeated EnumDescriptorProto enum_type = 4; + + repeated ExtensionRange extension_range = 5; + message ExtensionRange { + optional int32 start = 1; + optional int32 end = 2; + optional ExtensionRangeOptions options = 3; + } + + repeated OneofDescriptorProto oneof_decl = 8; + optional MessageOptions options = 7; + + repeated ReservedRange reserved_range = 9; + message ReservedRange { + optional int32 start = 1; + optional int32 end = 2; + } + + repeated string reserved_name = 10; +} + +message ExtensionRangeOptions { + repeated UninterpretedOption uninterpreted_option = 999; + + extensions 1000 to max; +} + +message FieldDescriptorProto { + optional string name = 1; + optional int32 number = 3; + + optional Label label = 4; + enum Label { + LABEL_OPTIONAL = 1; + LABEL_REQUIRED = 2; + LABEL_REPEATED = 3; + } + + optional Type type = 5; + enum Type { + TYPE_DOUBLE = 1; + TYPE_FLOAT = 2; + TYPE_INT64 = 3; + TYPE_UINT64 = 4; + TYPE_INT32 = 5; + TYPE_FIXED64 = 6; + TYPE_FIXED32 = 7; + TYPE_BOOL = 8; + TYPE_STRING = 9; + TYPE_GROUP = 10; + TYPE_MESSAGE = 11; + TYPE_BYTES = 12; + TYPE_UINT32 = 13; + TYPE_ENUM = 14; + TYPE_SFIXED32 = 15; + TYPE_SFIXED64 = 16; + TYPE_SINT32 = 17; + TYPE_SINT64 = 18; + } + + optional string type_name = 6; + optional string extendee = 2; + optional string default_value = 7; + optional int32 oneof_index = 9; + optional string json_name = 10; + optional FieldOptions options = 8; + optional bool proto3_optional = 17; +} + +message OneofDescriptorProto { + optional string name = 1; + optional OneofOptions options = 2; +} + +message EnumDescriptorProto { + optional string name = 1; + repeated EnumValueDescriptorProto value = 2; + optional EnumOptions options = 3; + + repeated EnumReservedRange reserved_range = 4; + message EnumReservedRange { + optional int32 start = 1; + optional int32 end = 2; + } + + repeated string reserved_name = 5; +} + +message EnumValueDescriptorProto { + optional string name = 1; + optional int32 number = 2; + optional EnumValueOptions options = 3; +} + +message ServiceDescriptorProto { + optional string name = 1; + repeated MethodDescriptorProto method = 2; + optional ServiceOptions options = 3; +} + +message MethodDescriptorProto { + optional string name = 1; + optional string input_type = 2; + optional string output_type = 3; + optional MethodOptions options = 4; + optional bool client_streaming = 5 [default = false]; + optional bool server_streaming = 6 [default = false]; +} + +message FileOptions { + optional string java_package = 1; + optional string java_outer_classname = 8; + optional bool java_multiple_files = 10 [default = false]; + optional bool java_generate_equals_and_hash = 20 [deprecated = true]; + optional bool java_string_check_utf8 = 27 [default = false]; + + optional OptimizeMode optimize_for = 9 [default = SPEED]; + enum OptimizeMode { + SPEED = 1; + CODE_SIZE = 2; + LITE_RUNTIME = 3; + } + + optional string go_package = 11; + optional bool cc_generic_services = 16 [default = false]; + optional bool java_generic_services = 17 [default = false]; + optional bool py_generic_services = 18 [default = false]; + optional bool php_generic_services = 42 [default = false]; + optional bool deprecated = 23 [default = false]; + optional bool cc_enable_arenas = 31 [default = true]; + optional string objc_class_prefix = 36; + optional string csharp_namespace = 37; + optional string swift_prefix = 39; + optional string php_class_prefix = 40; + optional string php_namespace = 41; + optional string php_metadata_namespace = 44; + optional string ruby_package = 45; + repeated UninterpretedOption uninterpreted_option = 999; + + extensions 1000 to max; + + reserved 38; +} + +message MessageOptions { + optional bool message_set_wire_format = 1 [default = false]; + optional bool no_standard_descriptor_accessor = 2 [default = false]; + optional bool deprecated = 3 [default = false]; + optional bool map_entry = 7; + repeated UninterpretedOption uninterpreted_option = 999; + + extensions 1000 to max; + + reserved 8, 9; +} + +message FieldOptions { + optional CType ctype = 1 [default = STRING]; + enum CType { + STRING = 0; + CORD = 1; + STRING_PIECE = 2; + } + + optional bool packed = 2; + + optional JSType jstype = 6 [default = JS_NORMAL]; + enum JSType { + JS_NORMAL = 0; + JS_STRING = 1; + JS_NUMBER = 2; + } + + optional bool lazy = 5 [default = false]; + optional bool deprecated = 3 [default = false]; + optional bool weak = 10 [default = false]; + repeated UninterpretedOption uninterpreted_option = 999; + + extensions 1000 to max; + + reserved 4; +} + +message OneofOptions { + repeated UninterpretedOption uninterpreted_option = 999; + + extensions 1000 to max; +} + +message EnumOptions { + optional bool allow_alias = 2; + optional bool deprecated = 3 [default = false]; + repeated UninterpretedOption uninterpreted_option = 999; + + extensions 1000 to max; + + reserved 5; +} + +message EnumValueOptions { + optional bool deprecated = 1 [default = false]; + repeated UninterpretedOption uninterpreted_option = 999; + + extensions 1000 to max; +} + +message ServiceOptions { + optional bool deprecated = 33 [default = false]; + repeated UninterpretedOption uninterpreted_option = 999; + + extensions 1000 to max; +} + +message MethodOptions { + optional bool deprecated = 33 [default = false]; + + optional IdempotencyLevel idempotency_level = 34 [default = IDEMPOTENCY_UNKNOWN]; + enum IdempotencyLevel { + IDEMPOTENCY_UNKNOWN = 0; + NO_SIDE_EFFECTS = 1; + IDEMPOTENT = 2; + } + + repeated UninterpretedOption uninterpreted_option = 999; + + extensions 1000 to max; +} + +message UninterpretedOption { + repeated NamePart name = 2; + message NamePart { + required string name_part = 1; + required bool is_extension = 2; + } + + optional string identifier_value = 3; + optional uint64 positive_int_value = 4; + optional int64 negative_int_value = 5; + optional double double_value = 6; + optional bytes string_value = 7; + optional string aggregate_value = 8; +} + +message SourceCodeInfo { + repeated Location location = 1; + message Location { + repeated int32 path = 1 [packed = true]; + repeated int32 span = 2 [packed = true]; + optional string leading_comments = 3; + optional string trailing_comments = 4; + repeated string leading_detached_comments = 6; + } +} + +message GeneratedCodeInfo { + repeated Annotation annotation = 1; + message Annotation { + repeated int32 path = 1 [packed = true]; + optional string source_file = 2; + optional int32 begin = 3; + optional int32 end = 4; + } +} diff --git a/protocol/proto/google/protobuf/duration.proto b/protocol/proto/google/protobuf/duration.proto new file mode 100644 index 00000000..f7d01301 --- /dev/null +++ b/protocol/proto/google/protobuf/duration.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +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/durationpb"; +option java_multiple_files = true; +option java_outer_classname = "DurationProto"; +option java_package = "com.google.protobuf"; + +message Duration { + int64 seconds = 1; + int32 nanos = 2; +} diff --git a/protocol/proto/google/protobuf/field_mask.proto b/protocol/proto/google/protobuf/field_mask.proto new file mode 100644 index 00000000..3ae48712 --- /dev/null +++ b/protocol/proto/google/protobuf/field_mask.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +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/fieldmaskpb"; +option java_multiple_files = true; +option java_outer_classname = "FieldMaskProto"; +option java_package = "com.google.protobuf"; + +message FieldMask { + repeated string paths = 1; +} diff --git a/protocol/proto/google/protobuf/source_context.proto b/protocol/proto/google/protobuf/source_context.proto new file mode 100644 index 00000000..8449fd4b --- /dev/null +++ b/protocol/proto/google/protobuf/source_context.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package google.protobuf; + +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; +option objc_class_prefix = "GPB"; +option go_package = "google.golang.org/protobuf/types/known/sourcecontextpb"; +option java_multiple_files = true; +option java_outer_classname = "SourceContextProto"; +option java_package = "com.google.protobuf"; + +message SourceContext { + string file_name = 1; +} diff --git a/protocol/proto/google/protobuf/timestamp.proto b/protocol/proto/google/protobuf/timestamp.proto new file mode 100644 index 00000000..e402c47a --- /dev/null +++ b/protocol/proto/google/protobuf/timestamp.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +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/timestamppb"; +option java_multiple_files = true; +option java_outer_classname = "TimestampProto"; +option java_package = "com.google.protobuf"; + +message Timestamp { + int64 seconds = 1; + int32 nanos = 2; +} diff --git a/protocol/proto/google/protobuf/type.proto b/protocol/proto/google/protobuf/type.proto new file mode 100644 index 00000000..38d7f2d1 --- /dev/null +++ b/protocol/proto/google/protobuf/type.proto @@ -0,0 +1,91 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package google.protobuf; + +import "google/protobuf/any.proto"; +import "google/protobuf/source_context.proto"; + +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/typepb"; +option java_multiple_files = true; +option java_outer_classname = "TypeProto"; +option java_package = "com.google.protobuf"; + +message Type { + string name = 1; + repeated Field fields = 2; + repeated string oneofs = 3; + repeated Option options = 4; + SourceContext source_context = 5; + Syntax syntax = 6; +} + +message Field { + Kind kind = 1; + enum Kind { + TYPE_UNKNOWN = 0; + TYPE_DOUBLE = 1; + TYPE_FLOAT = 2; + TYPE_INT64 = 3; + TYPE_UINT64 = 4; + TYPE_INT32 = 5; + TYPE_FIXED64 = 6; + TYPE_FIXED32 = 7; + TYPE_BOOL = 8; + TYPE_STRING = 9; + TYPE_GROUP = 10; + TYPE_MESSAGE = 11; + TYPE_BYTES = 12; + TYPE_UINT32 = 13; + TYPE_ENUM = 14; + TYPE_SFIXED32 = 15; + TYPE_SFIXED64 = 16; + TYPE_SINT32 = 17; + TYPE_SINT64 = 18; + } + + Cardinality cardinality = 2; + enum Cardinality { + CARDINALITY_UNKNOWN = 0; + CARDINALITY_OPTIONAL = 1; + CARDINALITY_REQUIRED = 2; + CARDINALITY_REPEATED = 3; + } + + int32 number = 3; + string name = 4; + string type_url = 6; + int32 oneof_index = 7; + bool packed = 8; + repeated Option options = 9; + string json_name = 10; + string default_value = 11; +} + +message Enum { + string name = 1; + repeated EnumValue enumvalue = 2; + repeated Option options = 3; + SourceContext source_context = 4; + Syntax syntax = 5; +} + +message EnumValue { + string name = 1; + int32 number = 2; + repeated Option options = 3; +} + +message Option { + string name = 1; + Any value = 2; +} + +enum Syntax { + SYNTAX_PROTO2 = 0; + SYNTAX_PROTO3 = 1; +} diff --git a/protocol/proto/google/protobuf/wrappers.proto b/protocol/proto/google/protobuf/wrappers.proto new file mode 100644 index 00000000..10e94ee0 --- /dev/null +++ b/protocol/proto/google/protobuf/wrappers.proto @@ -0,0 +1,49 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +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/wrapperspb"; +option java_multiple_files = true; +option java_outer_classname = "WrappersProto"; +option java_package = "com.google.protobuf"; + +message DoubleValue { + double value = 1; +} + +message FloatValue { + float value = 1; +} + +message Int64Value { + int64 value = 1; +} + +message UInt64Value { + uint64 value = 1; +} + +message Int32Value { + int32 value = 1; +} + +message UInt32Value { + uint32 value = 1; +} + +message BoolValue { + bool value = 1; +} + +message StringValue { + string value = 1; +} + +message BytesValue { + bytes value = 1; +} diff --git a/protocol/proto/identity.proto b/protocol/proto/identity.proto new file mode 100644 index 00000000..efd8b9e1 --- /dev/null +++ b/protocol/proto/identity.proto @@ -0,0 +1,37 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.identity.v3; + +import "google/protobuf/field_mask.proto"; +import "google/protobuf/wrappers.proto"; + +option optimize_for = CODE_SIZE; +option java_outer_classname = "IdentityV3"; +option java_package = "com.spotify.identity.proto.v3"; + +message Image { + int32 max_width = 1; + int32 max_height = 2; + string url = 3; +} + +message UserProfile { + google.protobuf.StringValue username = 1; + google.protobuf.StringValue name = 2; + repeated Image images = 3; + google.protobuf.BoolValue verified = 4; + google.protobuf.BoolValue edit_profile_disabled = 5; + google.protobuf.BoolValue report_abuse_disabled = 6; + google.protobuf.BoolValue abuse_reported_name = 7; + google.protobuf.BoolValue abuse_reported_image = 8; + google.protobuf.BoolValue has_spotify_name = 9; + google.protobuf.BoolValue has_spotify_image = 10; + google.protobuf.Int32Value color = 11; +} + +message UserProfileUpdateRequest { + google.protobuf.FieldMask mask = 1; + UserProfile user_profile = 2; +} diff --git a/protocol/proto/image-resolve.proto b/protocol/proto/image-resolve.proto new file mode 100644 index 00000000..d8befe97 --- /dev/null +++ b/protocol/proto/image-resolve.proto @@ -0,0 +1,33 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.imageresolve.proto; + +option java_multiple_files = true; +option java_outer_classname = "ImageResolveProtos"; +option java_package = "com.spotify.imageresolve.proto"; + +message Collection { + bytes id = 1; + + repeated Projection projections = 2; + message Projection { + bytes id = 2; + int32 metadata_index = 3; + int32 url_template_index = 4; + } +} + +message ProjectionMetadata { + int32 width = 2; + int32 height = 3; + bool fetch_online = 4; + bool download_for_offline = 5; +} + +message ProjectionMap { + repeated string url_templates = 1; + repeated ProjectionMetadata projection_metas = 2; + repeated Collection collections = 3; +} diff --git a/protocol/proto/installation_data.proto b/protocol/proto/installation_data.proto new file mode 100644 index 00000000..083fe466 --- /dev/null +++ b/protocol/proto/installation_data.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message InstallationEntity { + int32 file_format_version = 1; + bytes encrypted_part = 2; +} + +message InstallationData { + string installation_id = 1; + string last_seen_device_id = 2; + int64 monotonic_clock_id = 3; +} diff --git a/protocol/proto/instrumentation_params.proto b/protocol/proto/instrumentation_params.proto new file mode 100644 index 00000000..b8e44f4a --- /dev/null +++ b/protocol/proto/instrumentation_params.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto.transfer; + +option optimize_for = CODE_SIZE; + +message InstrumentationParams { + repeated string interaction_ids = 6; + repeated string page_instance_ids = 7; +} diff --git a/protocol/proto/lfs_secret_provider.proto b/protocol/proto/lfs_secret_provider.proto new file mode 100644 index 00000000..0862181e --- /dev/null +++ b/protocol/proto/lfs_secret_provider.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.lfssecretprovider.proto; + +option optimize_for = CODE_SIZE; + +message GetSecretResponse { + bytes secret = 1; +} diff --git a/protocol/proto/liked_songs_tags_sync_state.proto b/protocol/proto/liked_songs_tags_sync_state.proto new file mode 100644 index 00000000..634f9d03 --- /dev/null +++ b/protocol/proto/liked_songs_tags_sync_state.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.collection.proto; + +option optimize_for = CODE_SIZE; + +message TagsSyncState { + string uri = 1; + bool sync_is_complete = 2; +} diff --git a/protocol/proto/listen_later_cosmos_response.proto b/protocol/proto/listen_later_cosmos_response.proto new file mode 100644 index 00000000..f71c577c --- /dev/null +++ b/protocol/proto/listen_later_cosmos_response.proto @@ -0,0 +1,28 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.listen_later_cosmos.proto; + +import "collection/episode_collection_state.proto"; +import "metadata/episode_metadata.proto"; +import "played_state/episode_played_state.proto"; +import "sync/episode_sync_state.proto"; + +option optimize_for = CODE_SIZE; + +message Episode { + optional string header = 1; + optional cosmos_util.proto.EpisodeMetadata episode_metadata = 2; + optional cosmos_util.proto.EpisodeCollectionState episode_collection_state = 3; + optional cosmos_util.proto.EpisodeSyncState episode_offline_state = 4; + optional cosmos_util.proto.EpisodePlayState episode_played_state = 5; +} + +message EpisodesResponse { + optional uint32 unfiltered_length = 1; + optional uint32 unranged_length = 2; + repeated Episode episode = 3; + optional string offline_availability = 5; + optional uint32 offline_progress = 6; +} diff --git a/protocol/proto/local_bans_storage.proto b/protocol/proto/local_bans_storage.proto new file mode 100644 index 00000000..388f05b5 --- /dev/null +++ b/protocol/proto/local_bans_storage.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.collection.proto.storage; + +option optimize_for = CODE_SIZE; + +message BanItem { + required string item_uri = 1; + required string context_uri = 2; + required int64 timestamp = 3; +} + +message Bans { + repeated BanItem items = 1; +} diff --git a/protocol/proto/local_sync_cosmos.proto b/protocol/proto/local_sync_cosmos.proto new file mode 100644 index 00000000..cf6187f7 --- /dev/null +++ b/protocol/proto/local_sync_cosmos.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.local_sync_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message GetDevicesResponse { + repeated Device devices = 1; + message Device { + string name = 1; + string id = 2; + string endpoint = 3; + } +} diff --git a/protocol/proto/local_sync_state.proto b/protocol/proto/local_sync_state.proto new file mode 100644 index 00000000..630f1843 --- /dev/null +++ b/protocol/proto/local_sync_state.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.local_sync_state.proto; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.local_sync_state.proto"; + +message State { + string safe_secret = 1; +} diff --git a/protocol/proto/logging_params.proto b/protocol/proto/logging_params.proto new file mode 100644 index 00000000..1f11809d --- /dev/null +++ b/protocol/proto/logging_params.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message LoggingParams { + optional int64 command_initiated_time = 1; + optional int64 command_received_time = 2; + repeated string page_instance_ids = 3; + repeated string interaction_ids = 4; +} diff --git a/protocol/proto/mdata.proto b/protocol/proto/mdata.proto new file mode 100644 index 00000000..29faad9c --- /dev/null +++ b/protocol/proto/mdata.proto @@ -0,0 +1,43 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.mdata.proto; + +import "extension_kind.proto"; +import "google/protobuf/any.proto"; + +option cc_enable_arenas = true; +option optimize_for = CODE_SIZE; + +message LocalExtensionQuery { + extendedmetadata.ExtensionKind extension_kind = 1; + repeated string entity_uri = 2; +} + +message LocalBatchedEntityRequest { + repeated LocalExtensionQuery extension_query = 1; +} + +message LocalBatchedExtensionResponse { + repeated Extension extension = 1; + message Extension { + extendedmetadata.ExtensionKind extension_kind = 1; + repeated EntityExtension entity_extension = 2; + } + + message ExtensionHeader { + bool cache_valid = 1; + bool offline_valid = 2; + int32 status_code = 3; + bool is_empty = 4; + int64 cache_expiry_timestamp = 5; + int64 offline_expiry_timestamp = 6; + } + + message EntityExtension { + string entity_uri = 1; + ExtensionHeader header = 2; + google.protobuf.Any extension_data = 3; + } +} diff --git a/protocol/proto/mdata_cosmos.proto b/protocol/proto/mdata_cosmos.proto new file mode 100644 index 00000000..3c67357c --- /dev/null +++ b/protocol/proto/mdata_cosmos.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.mdata_cosmos.proto; + +import "extension_kind.proto"; + +option cc_enable_arenas = true; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.mdata.cosmos.proto"; + +message InvalidateCacheRequest { + extendedmetadata.ExtensionKind extension_kind = 1; + repeated string entity_uri = 2; +} + +message InvalidateCacheResponse { + +} diff --git a/protocol/proto/mdata_storage.proto b/protocol/proto/mdata_storage.proto new file mode 100644 index 00000000..8703fe54 --- /dev/null +++ b/protocol/proto/mdata_storage.proto @@ -0,0 +1,38 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.mdata.proto.storage; + +import "extension_kind.proto"; +import "google/protobuf/any.proto"; + +option cc_enable_arenas = true; +option optimize_for = CODE_SIZE; + +message CacheEntry { + extendedmetadata.ExtensionKind kind = 1; + google.protobuf.Any extension_data = 2; +} + +message CacheInfo { + int32 status_code = 1; + bool is_empty = 2; + uint64 cache_expiry = 3; + uint64 offline_expiry = 4; + string etag = 5; + fixed64 cache_checksum_lo = 6; + fixed64 cache_checksum_hi = 7; +} + +message OfflineLock { + uint64 lock_expiry = 1; +} + +message AudioFiles { + string file_id = 1; +} + +message TrackDescriptor { + int32 track_id = 1; +} diff --git a/protocol/proto/media_manifest.proto b/protocol/proto/media_manifest.proto new file mode 100644 index 00000000..a6a97681 --- /dev/null +++ b/protocol/proto/media_manifest.proto @@ -0,0 +1,55 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.media_manifest; + +option optimize_for = CODE_SIZE; + +message AudioFile { + enum Format { + OGG_VORBIS_96 = 0; + OGG_VORBIS_160 = 1; + OGG_VORBIS_320 = 2; + MP3_256 = 3; + MP3_320 = 4; + MP3_160 = 5; + MP3_96 = 6; + MP3_160_ENC = 7; + AAC_24 = 8; + AAC_48 = 9; + FLAC_FLAC = 16; + } +} + +message File { + int32 bitrate = 3; + string mime_type = 4; + + oneof file { + ExternalFile external_file = 1; + FileIdFile file_id_file = 2; + } + + message ExternalFile { + string method = 1; + string url = 2; + bytes body = 3; + bool is_webgate_endpoint = 4; + } + + message FileIdFile { + string file_id_hex = 1; + AudioFile.Format download_format = 2; + EncryptionType encryption = 3; + } +} + +message Files { + repeated File files = 1; +} + +enum EncryptionType { + NONE = 0; + AES = 1; +} diff --git a/protocol/proto/media_type.proto b/protocol/proto/media_type.proto new file mode 100644 index 00000000..5527922f --- /dev/null +++ b/protocol/proto/media_type.proto @@ -0,0 +1,14 @@ +// 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 MediaType { + MEDIA_TYPE_UNSET = 0; + AUDIO = 1; + VIDEO = 2; +} diff --git a/protocol/proto/media_type_node.proto b/protocol/proto/media_type_node.proto new file mode 100644 index 00000000..0d0a5964 --- /dev/null +++ b/protocol/proto/media_type_node.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message MediaTypeNode { + optional string current_uri = 1; + optional string media_type = 2; + optional string media_manifest_id = 3; +} diff --git a/protocol/proto/mergedprofile.proto b/protocol/proto/mergedprofile.proto deleted file mode 100644 index e283e1de..00000000 --- a/protocol/proto/mergedprofile.proto +++ /dev/null @@ -1,10 +0,0 @@ -syntax = "proto2"; - -message MergedProfileRequest { -} - -message MergedProfileReply { - optional string username = 0x1; - optional string artistid = 0x2; -} - diff --git a/protocol/proto/metadata.proto b/protocol/proto/metadata.proto index 3812f94e..a6d3aded 100644 --- a/protocol/proto/metadata.proto +++ b/protocol/proto/metadata.proto @@ -1,266 +1,311 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + syntax = "proto2"; -message TopTracks { - optional string country = 0x1; - repeated Track track = 0x2; -} +package spotify.metadata; -message ActivityPeriod { - optional sint32 start_year = 0x1; - optional sint32 end_year = 0x2; - optional sint32 decade = 0x3; -} +option optimize_for = CODE_SIZE; +option java_outer_classname = "Metadata"; +option java_package = "com.spotify.metadata.proto"; message Artist { - optional bytes gid = 0x1; - optional string name = 0x2; - optional sint32 popularity = 0x3; - repeated TopTracks top_track = 0x4; - repeated AlbumGroup album_group = 0x5; - repeated AlbumGroup single_group = 0x6; - repeated AlbumGroup compilation_group = 0x7; - repeated AlbumGroup appears_on_group = 0x8; - repeated string genre = 0x9; - repeated ExternalId external_id = 0xa; - repeated Image portrait = 0xb; - repeated Biography biography = 0xc; - repeated ActivityPeriod activity_period = 0xd; - repeated Restriction restriction = 0xe; - repeated Artist related = 0xf; - optional bool is_portrait_album_cover = 0x10; - optional ImageGroup portrait_group = 0x11; -} - -message AlbumGroup { - repeated Album album = 0x1; -} - -message Date { - optional sint32 year = 0x1; - optional sint32 month = 0x2; - optional sint32 day = 0x3; - optional sint32 hour = 0x4; - optional sint32 minute = 0x5; + optional bytes gid = 1; + optional string name = 2; + optional sint32 popularity = 3; + repeated TopTracks top_track = 4; + repeated AlbumGroup album_group = 5; + repeated AlbumGroup single_group = 6; + repeated AlbumGroup compilation_group = 7; + repeated AlbumGroup appears_on_group = 8; + repeated string genre = 9; + repeated ExternalId external_id = 10; + repeated Image portrait = 11; + repeated Biography biography = 12; + repeated ActivityPeriod activity_period = 13; + repeated Restriction restriction = 14; + repeated Artist related = 15; + optional bool is_portrait_album_cover = 16; + optional ImageGroup portrait_group = 17; + repeated SalePeriod sale_period = 18; + repeated Availability availability = 20; } message Album { - optional bytes gid = 0x1; - optional string name = 0x2; - repeated Artist artist = 0x3; - optional Type typ = 0x4; + optional bytes gid = 1; + optional string name = 2; + repeated Artist artist = 3; + + optional Type type = 4; enum Type { - ALBUM = 0x1; - SINGLE = 0x2; - COMPILATION = 0x3; - EP = 0x4; + ALBUM = 1; + SINGLE = 2; + COMPILATION = 3; + EP = 4; + AUDIOBOOK = 5; + PODCAST = 6; } - optional string label = 0x5; - optional Date date = 0x6; - optional sint32 popularity = 0x7; - repeated string genre = 0x8; - repeated Image cover = 0x9; - repeated ExternalId external_id = 0xa; - repeated Disc disc = 0xb; - repeated string review = 0xc; - repeated Copyright copyright = 0xd; - repeated Restriction restriction = 0xe; - repeated Album related = 0xf; - repeated SalePeriod sale_period = 0x10; - optional ImageGroup cover_group = 0x11; + + optional string label = 5; + optional Date date = 6; + optional sint32 popularity = 7; + repeated string genre = 8; + repeated Image cover = 9; + repeated ExternalId external_id = 10; + repeated Disc disc = 11; + repeated string review = 12; + repeated Copyright copyright = 13; + repeated Restriction restriction = 14; + repeated Album related = 15; + repeated SalePeriod sale_period = 16; + optional ImageGroup cover_group = 17; + optional string original_title = 18; + optional string version_title = 19; + optional string type_str = 20; + repeated Availability availability = 23; } message Track { - optional bytes gid = 0x1; - optional string name = 0x2; - optional Album album = 0x3; - repeated Artist artist = 0x4; - optional sint32 number = 0x5; - optional sint32 disc_number = 0x6; - optional sint32 duration = 0x7; - optional sint32 popularity = 0x8; - optional bool explicit = 0x9; - repeated ExternalId external_id = 0xa; - repeated Restriction restriction = 0xb; - repeated AudioFile file = 0xc; - repeated Track alternative = 0xd; - repeated SalePeriod sale_period = 0xe; - repeated AudioFile preview = 0xf; + optional bytes gid = 1; + optional string name = 2; + optional Album album = 3; + repeated Artist artist = 4; + optional sint32 number = 5; + optional sint32 disc_number = 6; + optional sint32 duration = 7; + optional sint32 popularity = 8; + optional bool explicit = 9; + repeated ExternalId external_id = 10; + repeated Restriction restriction = 11; + repeated AudioFile file = 12; + repeated Track alternative = 13; + repeated SalePeriod sale_period = 14; + repeated AudioFile preview = 15; + repeated string tags = 16; + optional int64 earliest_live_timestamp = 17; + optional bool has_lyrics = 18; + repeated Availability availability = 19; + optional Licensor licensor = 21; + repeated string language_of_performance = 22; + repeated ContentRating content_rating = 25; + optional string original_title = 27; + optional string version_title = 28; + repeated ArtistWithRole artist_with_role = 32; +} + +message ArtistWithRole { + optional bytes artist_gid = 1; + optional string artist_name = 2; + + optional ArtistRole role = 3; + enum ArtistRole { + ARTIST_ROLE_UNKNOWN = 0; + ARTIST_ROLE_MAIN_ARTIST = 1; + ARTIST_ROLE_FEATURED_ARTIST = 2; + ARTIST_ROLE_REMIXER = 3; + ARTIST_ROLE_ACTOR = 4; + ARTIST_ROLE_COMPOSER = 5; + ARTIST_ROLE_CONDUCTOR = 6; + ARTIST_ROLE_ORCHESTRA = 7; + } +} + +message Show { + optional bytes gid = 1; + optional string name = 2; + optional string description = 64; + optional sint32 deprecated_popularity = 65; + optional string publisher = 66; + optional string language = 67; + optional bool explicit = 68; + optional ImageGroup cover_image = 69; + repeated Episode episode = 70; + repeated Copyright copyright = 71; + repeated Restriction restriction = 72; + repeated string keyword = 73; + + optional MediaType media_type = 74; + enum MediaType { + MIXED = 0; + AUDIO = 1; + VIDEO = 2; + } + + optional ConsumptionOrder consumption_order = 75; + enum ConsumptionOrder { + SEQUENTIAL = 1; + EPISODIC = 2; + RECENT = 3; + } + + repeated Availability availability = 78; + optional string trailer_uri = 83; + optional bool music_and_talk = 85; +} + +message Episode { + optional bytes gid = 1; + optional string name = 2; + optional sint32 duration = 7; + repeated AudioFile audio = 12; + optional string description = 64; + optional sint32 number = 65; + optional Date publish_time = 66; + optional sint32 deprecated_popularity = 67; + optional ImageGroup cover_image = 68; + optional string language = 69; + optional bool explicit = 70; + optional Show show = 71; + repeated VideoFile video = 72; + repeated VideoFile video_preview = 73; + repeated AudioFile audio_preview = 74; + repeated Restriction restriction = 75; + optional ImageGroup freeze_frame = 76; + repeated string keyword = 77; + optional bool allow_background_playback = 81; + repeated Availability availability = 82; + optional string external_url = 83; + + optional EpisodeType type = 87; + enum EpisodeType { + FULL = 0; + TRAILER = 1; + BONUS = 2; + } + + optional bool music_and_talk = 91; +} + +message Licensor { + optional bytes uuid = 1; +} + +message TopTracks { + optional string country = 1; + repeated Track track = 2; +} + +message ActivityPeriod { + optional sint32 start_year = 1; + optional sint32 end_year = 2; + optional sint32 decade = 3; +} + +message AlbumGroup { + repeated Album album = 1; +} + +message Date { + optional sint32 year = 1; + optional sint32 month = 2; + optional sint32 day = 3; + optional sint32 hour = 4; + optional sint32 minute = 5; } message Image { - optional bytes file_id = 0x1; - optional Size size = 0x2; + optional bytes file_id = 1; + + optional Size size = 2; enum Size { - DEFAULT = 0x0; - SMALL = 0x1; - LARGE = 0x2; - XLARGE = 0x3; + DEFAULT = 0; + SMALL = 1; + LARGE = 2; + XLARGE = 3; } - optional sint32 width = 0x3; - optional sint32 height = 0x4; + + optional sint32 width = 3; + optional sint32 height = 4; } message ImageGroup { - repeated Image image = 0x1; + repeated Image image = 1; } message Biography { - optional string text = 0x1; - repeated Image portrait = 0x2; - repeated ImageGroup portrait_group = 0x3; + optional string text = 1; + repeated Image portrait = 2; + repeated ImageGroup portrait_group = 3; } message Disc { - optional sint32 number = 0x1; - optional string name = 0x2; - repeated Track track = 0x3; + optional sint32 number = 1; + optional string name = 2; + repeated Track track = 3; } message Copyright { - optional Type typ = 0x1; + optional Type type = 1; enum Type { - P = 0x0; - C = 0x1; + P = 0; + C = 1; } - optional string text = 0x2; + + optional string text = 2; } message Restriction { + repeated Catalogue catalogue = 1; enum Catalogue { - AD = 0; - SUBSCRIPTION = 1; - CATALOGUE_ALL = 2; - SHUFFLE = 3; - COMMERCIAL = 4; + AD = 0; + SUBSCRIPTION = 1; + CATALOGUE_ALL = 2; + SHUFFLE = 3; + COMMERCIAL = 4; } + + optional Type type = 4; enum Type { - STREAMING = 0x0; + STREAMING = 0; + } + + repeated string catalogue_str = 5; + + oneof country_restriction { + string countries_allowed = 2; + string countries_forbidden = 3; } - repeated Catalogue catalogue = 0x1; - optional string countries_allowed = 0x2; - optional string countries_forbidden = 0x3; - optional Type typ = 0x4; - - repeated string catalogue_str = 0x5; } message Availability { - repeated string catalogue_str = 0x1; - optional Date start = 0x2; + repeated string catalogue_str = 1; + optional Date start = 2; } message SalePeriod { - repeated Restriction restriction = 0x1; - optional Date start = 0x2; - optional Date end = 0x3; + repeated Restriction restriction = 1; + optional Date start = 2; + optional Date end = 3; } message ExternalId { - optional string typ = 0x1; - optional string id = 0x2; + optional string type = 1; + optional string id = 2; } message AudioFile { - optional bytes file_id = 0x1; - optional Format format = 0x2; + optional bytes file_id = 1; + + optional Format format = 2; enum Format { - OGG_VORBIS_96 = 0x0; - OGG_VORBIS_160 = 0x1; - OGG_VORBIS_320 = 0x2; - MP3_256 = 0x3; - MP3_320 = 0x4; - MP3_160 = 0x5; - MP3_96 = 0x6; - MP3_160_ENC = 0x7; - // v4 - // AAC_24 = 0x8; - // AAC_48 = 0x9; - MP4_128_DUAL = 0x8; - OTHER3 = 0x9; - AAC_160 = 0xa; - AAC_320 = 0xb; - MP4_128 = 0xc; - OTHER5 = 0xd; + OGG_VORBIS_96 = 0; + OGG_VORBIS_160 = 1; + OGG_VORBIS_320 = 2; + MP3_256 = 3; + MP3_320 = 4; + MP3_160 = 5; + MP3_96 = 6; + MP3_160_ENC = 7; + AAC_24 = 8; + AAC_48 = 9; + FLAC_FLAC = 16; } } message VideoFile { - optional bytes file_id = 1; + optional bytes file_id = 1; } -// Podcast Protos -message Show { - enum MediaType { - MIXED = 0; - AUDIO = 1; - VIDEO = 2; - } - enum ConsumptionOrder { - SEQUENTIAL = 1; - EPISODIC = 2; - RECENT = 3; - } - enum PassthroughEnum { - UNKNOWN = 0; - NONE = 1; - ALLOWED = 2; - } - optional bytes gid = 0x1; - optional string name = 0x2; - optional string description = 0x40; - optional sint32 deprecated_popularity = 0x41; - optional string publisher = 0x42; - optional string language = 0x43; - optional bool explicit = 0x44; - optional ImageGroup covers = 0x45; - repeated Episode episode = 0x46; - repeated Copyright copyright = 0x47; - repeated Restriction restriction = 0x48; - repeated string keyword = 0x49; - optional MediaType media_type = 0x4A; - optional ConsumptionOrder consumption_order = 0x4B; - optional bool interpret_restriction_using_geoip = 0x4C; - repeated Availability availability = 0x4E; - optional string country_of_origin = 0x4F; - repeated Category categories = 0x50; - optional PassthroughEnum passthrough = 0x51; -} - -message Episode { - optional bytes gid = 0x1; - optional string name = 0x2; - optional sint32 duration = 0x7; - optional sint32 popularity = 0x8; - repeated AudioFile file = 0xc; - optional string description = 0x40; - optional sint32 number = 0x41; - optional Date publish_time = 0x42; - optional sint32 deprecated_popularity = 0x43; - optional ImageGroup covers = 0x44; - optional string language = 0x45; - optional bool explicit = 0x46; - optional Show show = 0x47; - repeated VideoFile video = 0x48; - repeated VideoFile video_preview = 0x49; - repeated AudioFile audio_preview = 0x4A; - repeated Restriction restriction = 0x4B; - optional ImageGroup freeze_frame = 0x4C; - repeated string keyword = 0x4D; - // Order of these two flags might be wrong! - optional bool suppress_monetization = 0x4E; - optional bool interpret_restriction_using_geoip = 0x4F; - - optional bool allow_background_playback = 0x51; - repeated Availability availability = 0x52; - optional string external_url = 0x53; - optional OriginalAudio original_audio = 0x54; -} - -message Category { - optional string name = 0x1; - repeated Category subcategories = 0x2; -} - -message OriginalAudio { - optional bytes uuid = 0x1; +message ContentRating { + optional string country = 1; + repeated string tag = 2; } diff --git a/protocol/proto/metadata/album_metadata.proto b/protocol/proto/metadata/album_metadata.proto new file mode 100644 index 00000000..5a7de5f9 --- /dev/null +++ b/protocol/proto/metadata/album_metadata.proto @@ -0,0 +1,29 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +import "metadata/image_group.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message AlbumArtistMetadata { + optional string link = 1; + optional string name = 2; +} + +message AlbumMetadata { + repeated AlbumArtistMetadata artists = 1; + optional string link = 2; + optional string name = 3; + repeated string copyright = 4; + optional ImageGroup covers = 5; + optional uint32 year = 6; + optional uint32 num_discs = 7; + optional uint32 num_tracks = 8; + optional bool playability = 9; + optional bool is_premium_only = 10; +} diff --git a/protocol/proto/metadata/artist_metadata.proto b/protocol/proto/metadata/artist_metadata.proto new file mode 100644 index 00000000..4e5e9bfe --- /dev/null +++ b/protocol/proto/metadata/artist_metadata.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +import "metadata/image_group.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message ArtistMetadata { + optional string link = 1; + optional string name = 2; + optional bool is_various_artists = 3; + optional ImageGroup portraits = 4; +} diff --git a/protocol/proto/metadata/episode_metadata.proto b/protocol/proto/metadata/episode_metadata.proto new file mode 100644 index 00000000..9f47deee --- /dev/null +++ b/protocol/proto/metadata/episode_metadata.proto @@ -0,0 +1,59 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +import "metadata/image_group.proto"; +import "podcast_segments.proto"; +import "podcast_subscription.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message EpisodeShowMetadata { + optional string link = 1; + optional string name = 2; + optional string publisher = 3; + optional ImageGroup covers = 4; +} + +message EpisodeMetadata { + optional EpisodeShowMetadata show = 1; + optional string link = 2; + optional string name = 3; + optional uint32 length = 4; + optional ImageGroup covers = 5; + optional string manifest_id = 6; + optional string description = 7; + optional int64 publish_date = 8; + optional ImageGroup freeze_frames = 9; + optional string language = 10; + optional bool available = 11; + + optional MediaType media_type_enum = 12; + enum MediaType { + VODCAST = 0; + AUDIO = 1; + VIDEO = 2; + } + + optional int32 number = 13; + optional bool backgroundable = 14; + optional string preview_manifest_id = 15; + optional bool is_explicit = 16; + optional string preview_id = 17; + + optional EpisodeType episode_type = 18; + enum EpisodeType { + UNKNOWN = 0; + FULL = 1; + TRAILER = 2; + BONUS = 3; + } + + optional bool is_music_and_talk = 19; + optional podcast_segments.PodcastSegments podcast_segments = 20; + optional podcast_paywalls.PodcastSubscription podcast_subscription = 21; +} diff --git a/protocol/proto/metadata/image_group.proto b/protocol/proto/metadata/image_group.proto new file mode 100644 index 00000000..310a408b --- /dev/null +++ b/protocol/proto/metadata/image_group.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message ImageGroup { + optional string standard_link = 1; + optional string small_link = 2; + optional string large_link = 3; + optional string xlarge_link = 4; +} diff --git a/protocol/proto/metadata/show_metadata.proto b/protocol/proto/metadata/show_metadata.proto new file mode 100644 index 00000000..8beaf97b --- /dev/null +++ b/protocol/proto/metadata/show_metadata.proto @@ -0,0 +1,28 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +import "metadata/image_group.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message ShowMetadata { + optional string link = 1; + optional string name = 2; + optional string description = 3; + optional uint32 popularity = 4; + optional string publisher = 5; + optional string language = 6; + optional bool is_explicit = 7; + optional ImageGroup covers = 8; + optional uint32 num_episodes = 9; + optional string consumption_order = 10; + optional int32 media_type_enum = 11; + repeated string copyright = 12; + optional string trailer_uri = 13; + optional bool is_music_and_talk = 14; +} diff --git a/protocol/proto/metadata/track_metadata.proto b/protocol/proto/metadata/track_metadata.proto new file mode 100644 index 00000000..08bff401 --- /dev/null +++ b/protocol/proto/metadata/track_metadata.proto @@ -0,0 +1,55 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +import "metadata/image_group.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message TrackAlbumArtistMetadata { + optional string link = 1; + optional string name = 2; +} + +message TrackAlbumMetadata { + optional TrackAlbumArtistMetadata artist = 1; + optional string link = 2; + optional string name = 3; + optional ImageGroup covers = 4; +} + +message TrackArtistMetadata { + optional string link = 1; + optional string name = 2; + optional ImageGroup portraits = 3; +} + +message TrackDescriptor { + optional string name = 1; +} + +message TrackMetadata { + optional TrackAlbumMetadata album = 1; + repeated TrackArtistMetadata artist = 2; + optional string link = 3; + optional string name = 4; + optional uint32 length = 5; + optional bool playable = 6; + optional uint32 disc_number = 7; + optional uint32 track_number = 8; + optional bool is_explicit = 9; + optional string preview_id = 10; + optional bool is_local = 11; + optional bool playable_local_track = 12; + optional bool has_lyrics = 13; + optional bool is_premium_only = 14; + optional bool locally_playable = 15; + optional string playable_track_link = 16; + optional uint32 popularity = 17; + optional bool is_19_plus_only = 18; + repeated TrackDescriptor track_descriptors = 19; +} diff --git a/protocol/proto/metadata_cosmos.proto b/protocol/proto/metadata_cosmos.proto new file mode 100644 index 00000000..f04e5957 --- /dev/null +++ b/protocol/proto/metadata_cosmos.proto @@ -0,0 +1,30 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.metadata_cosmos.proto; + +import "metadata.proto"; + +option optimize_for = CODE_SIZE; +option java_outer_classname = "MetadataCosmos"; +option java_package = "com.spotify.metadata.cosmos.proto"; + +message MetadataItem { + oneof item { + sint32 error = 1; + metadata.Artist artist = 2; + metadata.Album album = 3; + metadata.Track track = 4; + metadata.Show show = 5; + metadata.Episode episode = 6; + } +} + +message MultiResponse { + repeated MetadataItem items = 1; +} + +message MultiRequest { + repeated string uris = 1; +} diff --git a/protocol/proto/modification_request.proto b/protocol/proto/modification_request.proto new file mode 100644 index 00000000..d35b613c --- /dev/null +++ b/protocol/proto/modification_request.proto @@ -0,0 +1,37 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message ModificationRequest { + optional string operation = 1; + optional string before = 2; + optional string after = 3; + optional string name = 4; + optional bool playlist = 5; + + optional Attributes attributes = 6; + message Attributes { + optional bool published = 1; + optional bool collaborative = 2; + optional string name = 3; + optional string description = 4; + optional string imageUri = 5; + optional string picture = 6; + } + + repeated string uris = 7; + repeated string rows = 8; + optional bool contents = 9; + optional string item_id = 10; +} + +message ModificationResponse { + optional bool success = 1; + optional string uri = 2; +} diff --git a/protocol/proto/net-fortune.proto b/protocol/proto/net-fortune.proto new file mode 100644 index 00000000..dbf476b2 --- /dev/null +++ b/protocol/proto/net-fortune.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.netfortune.proto; + +option optimize_for = CODE_SIZE; + +message NetFortuneResponse { + int32 advised_audio_bitrate = 1; +} + +message NetFortuneV2Response { + string predict_id = 1; + int32 estimated_max_bitrate = 2; +} diff --git a/protocol/proto/offline.proto b/protocol/proto/offline.proto new file mode 100644 index 00000000..b3d12491 --- /dev/null +++ b/protocol/proto/offline.proto @@ -0,0 +1,83 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.offline.proto; + +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; + +option optimize_for = CODE_SIZE; + +message Capacity { + double total_space = 1; + double free_space = 2; + double offline_space = 3; + uint64 track_count = 4; + uint64 episode_count = 5; +} + +message Device { + string device_id = 1; + string cache_id = 2; + string name = 3; + int32 type = 4; + int32 platform = 5; + bool offline_enabled = 6; + Capacity capacity = 7; + google.protobuf.Timestamp updated_at = 9; + google.protobuf.Timestamp last_seen_at = 10; + bool removal_pending = 11; +} + +message Restrictions { + google.protobuf.Duration allowed_duration_tracks = 1; + uint64 max_tracks = 2; + google.protobuf.Duration allowed_duration_episodes = 3; + uint64 max_episodes = 4; +} + +message Resource { + string uri = 1; + ResourceState state = 2; + int32 progress = 3; + google.protobuf.Timestamp updated_at = 4; + string failure_message = 5; +} + +message DeviceKey { + string user_id = 1; + string device_id = 2; + string cache_id = 3; +} + +message ResourceForDevice { + string device_id = 1; + string cache_id = 2; + Resource resource = 3; +} + +message ResourceOperation { + Operation operation = 2; + enum Operation { + INVALID = 0; + ADD = 1; + REMOVE = 2; + } + + string uri = 3; +} + +message ResourceHistoryItem { + repeated ResourceOperation operations = 1; + google.protobuf.Timestamp server_time = 2; +} + +enum ResourceState { + UNSPECIFIED = 0; + REQUESTED = 1; + PENDING = 2; + DOWNLOADING = 3; + DOWNLOADED = 4; + FAILURE = 5; +} diff --git a/protocol/proto/offline_playlists_containing.proto b/protocol/proto/offline_playlists_containing.proto new file mode 100644 index 00000000..19106b0c --- /dev/null +++ b/protocol/proto/offline_playlists_containing.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message OfflinePlaylistContainingItem { + required string playlist_link = 1; + optional string playlist_name = 2; +} + +message OfflinePlaylistsContainingItemResponse { + repeated OfflinePlaylistContainingItem playlists = 1; +} diff --git a/protocol/proto/on_demand_in_free_reason.proto b/protocol/proto/on_demand_in_free_reason.proto new file mode 100644 index 00000000..bf3a820e --- /dev/null +++ b/protocol/proto/on_demand_in_free_reason.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.on_demand_set.proto; + +option optimize_for = CODE_SIZE; + +enum OnDemandInFreeReason { + UNKNOWN = 0; + NOT_ON_DEMAND = 1; + ON_DEMAND = 2; + ON_DEMAND_EPISODES_ONLY = 3; +} diff --git a/protocol/proto/on_demand_set_cosmos_request.proto b/protocol/proto/on_demand_set_cosmos_request.proto new file mode 100644 index 00000000..28b70c16 --- /dev/null +++ b/protocol/proto/on_demand_set_cosmos_request.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.on_demand_set_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message Set { + repeated string uris = 1; +} + +message Temporary { + optional string uri = 1; + optional int64 valid_for_in_seconds = 2; +} diff --git a/protocol/proto/on_demand_set_cosmos_response.proto b/protocol/proto/on_demand_set_cosmos_response.proto new file mode 100644 index 00000000..3e5d708f --- /dev/null +++ b/protocol/proto/on_demand_set_cosmos_response.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.on_demand_set_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message Response { + optional bool success = 1; +} diff --git a/protocol/proto/pin_request.proto b/protocol/proto/pin_request.proto new file mode 100644 index 00000000..23e064ad --- /dev/null +++ b/protocol/proto/pin_request.proto @@ -0,0 +1,33 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.your_library.proto; + +option optimize_for = CODE_SIZE; + +message PinRequest { + string uri = 1; +} + +message PinResponse { + PinStatus status = 1; + enum PinStatus { + UNKNOWN = 0; + PINNED = 1; + NOT_PINNED = 2; + } + + bool has_maximum_pinned_items = 2; + string error = 99; +} + +message PinItem { + string uri = 1; + bool in_library = 2; +} + +message PinList { + repeated PinItem item = 1; + string error = 99; +} diff --git a/protocol/proto/play_origin.proto b/protocol/proto/play_origin.proto new file mode 100644 index 00000000..53bb0706 --- /dev/null +++ b/protocol/proto/play_origin.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message PlayOrigin { + optional string feature_identifier = 1; + optional string feature_version = 2; + optional string view_uri = 3; + optional string external_referrer = 4; + optional string referrer_identifier = 5; + optional string device_identifier = 6; + repeated string feature_classes = 7; +} diff --git a/protocol/proto/play_queue_node.proto b/protocol/proto/play_queue_node.proto new file mode 100644 index 00000000..d79a9825 --- /dev/null +++ b/protocol/proto/play_queue_node.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +import "context_track.proto"; +import "track_instance.proto"; +import "track_instantiator.proto"; + +option optimize_for = CODE_SIZE; + +message PlayQueueNode { + repeated ContextTrack queue = 1; + optional TrackInstance instance = 2; + optional TrackInstantiator instantiator = 3; + optional uint32 next_uid = 4; + optional sint32 iteration = 5; +} diff --git a/protocol/proto/play_reason.proto b/protocol/proto/play_reason.proto new file mode 100644 index 00000000..6ebfc914 --- /dev/null +++ b/protocol/proto/play_reason.proto @@ -0,0 +1,33 @@ +// 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 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; +} diff --git a/protocol/proto/play_source.proto b/protocol/proto/play_source.proto new file mode 100644 index 00000000..e4db2b9a --- /dev/null +++ b/protocol/proto/play_source.proto @@ -0,0 +1,47 @@ +// 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.proto b/protocol/proto/playback.proto new file mode 100644 index 00000000..94d8ae7e --- /dev/null +++ b/protocol/proto/playback.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto.transfer; + +import "context_track.proto"; + +option optimize_for = CODE_SIZE; + +message Playback { + optional int64 timestamp = 1; + optional int32 position_as_of_timestamp = 2; + optional double playback_speed = 3; + optional bool is_paused = 4; + optional ContextTrack current_track = 5; +} diff --git a/protocol/proto/playback_cosmos.proto b/protocol/proto/playback_cosmos.proto new file mode 100644 index 00000000..83a905fd --- /dev/null +++ b/protocol/proto/playback_cosmos.proto @@ -0,0 +1,105 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playback_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message VolumeRequest { + oneof source_or_system { + VolumeChangeSource source = 1; + bool system_initiated = 4; + } + + oneof action { + double volume = 2; + Step step = 3; + } + + enum Step { + option allow_alias = true; + up = 0; + UP = 0; + down = 1; + DOWN = 1; + } +} + +message VolumeResponse { + double volume = 1; +} + +message VolumeSubResponse { + double volume = 1; + VolumeChangeSource source = 2; + bool system_initiated = 3; +} + +message PositionResponseV1 { + int32 position = 1; +} + +message PositionResponseV2 { + int64 position = 1; +} + +message InfoResponse { + bool has_info = 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 file_origin = 16; + string strategy = 17; + int32 target_bitrate = 18; + int32 advised_bitrate = 19; + bool target_file_available = 20; +} + +message FormatsResponse { + 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 GetFilesResponse { + repeated File files = 1; + 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; +} + +enum VolumeChangeSource { + USER = 0; + SYSTEM = 1; +} diff --git a/protocol/proto/playback_segments.proto b/protocol/proto/playback_segments.proto new file mode 100644 index 00000000..1f6f6ea8 --- /dev/null +++ b/protocol/proto/playback_segments.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.podcast_segments.playback; + +import "podcast_segments.proto"; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "PlaybackSegmentsProto"; +option java_package = "com.spotify.podcastsegments.playback.proto"; + +message PlaybackSegments { + repeated PlaybackSegment playback_segments = 1; +} diff --git a/protocol/proto/played_state.proto b/protocol/proto/played_state.proto new file mode 100644 index 00000000..58990254 --- /dev/null +++ b/protocol/proto/played_state.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.played_state.proto; + +option optimize_for = CODE_SIZE; + +message PlayedStateItem { + optional string show_uri = 1; + optional string episode_uri = 2; + optional int32 resume_point = 3; + optional int32 last_played_at = 4; + optional bool is_latest = 5; + optional bool has_been_fully_played = 6; + optional bool has_been_synced = 7; + optional int32 episode_length = 8; +} + +message PlayedStateItems { + repeated PlayedStateItem item = 1; + optional uint64 last_server_sync_timestamp = 2; +} diff --git a/protocol/proto/played_state/episode_played_state.proto b/protocol/proto/played_state/episode_played_state.proto new file mode 100644 index 00000000..696b2e7a --- /dev/null +++ b/protocol/proto/played_state/episode_played_state.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +import "played_state/playability_restriction.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message EpisodePlayState { + optional uint32 time_left = 1; + optional bool is_playable = 2; + optional bool is_played = 3; + optional uint32 last_played_at = 4; + optional PlayabilityRestriction playability_restriction = 5 [default = UNKNOWN]; +} diff --git a/protocol/proto/played_state/playability_restriction.proto b/protocol/proto/played_state/playability_restriction.proto new file mode 100644 index 00000000..d6de6f4e --- /dev/null +++ b/protocol/proto/played_state/playability_restriction.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +enum PlayabilityRestriction { + UNKNOWN = 0; + NO_RESTRICTION = 1; + EXPLICIT_CONTENT = 2; + AGE_RESTRICTED = 3; + NOT_IN_CATALOGUE = 4; + NOT_AVAILABLE_OFFLINE = 5; +} diff --git a/protocol/proto/played_state/show_played_state.proto b/protocol/proto/played_state/show_played_state.proto new file mode 100644 index 00000000..08910f93 --- /dev/null +++ b/protocol/proto/played_state/show_played_state.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message ShowPlayState { + optional string latest_played_episode_link = 1; +} diff --git a/protocol/proto/played_state/track_played_state.proto b/protocol/proto/played_state/track_played_state.proto new file mode 100644 index 00000000..3a8e4c86 --- /dev/null +++ b/protocol/proto/played_state/track_played_state.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +import "played_state/playability_restriction.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message TrackPlayState { + optional bool is_playable = 1; + optional PlayabilityRestriction playability_restriction = 2 [default = UNKNOWN]; +} diff --git a/protocol/proto/playedstate.proto b/protocol/proto/playedstate.proto new file mode 100644 index 00000000..fefce00f --- /dev/null +++ b/protocol/proto/playedstate.proto @@ -0,0 +1,40 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify_playedstate.proto; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playedstate.proto"; + +message PlayedStateItem { + optional Type type = 1; + optional bytes uri = 2; + optional int64 client_timestamp = 3; + optional int32 play_position = 4; + optional bool played = 5; + optional int32 duration = 6; +} + +message PlayedState { + optional int64 server_timestamp = 1; + optional bool truncated = 2; + repeated PlayedStateItem state = 3; +} + +message PlayedStateItemList { + repeated PlayedStateItem state = 1; +} + +message ContentId { + optional Type type = 1; + optional bytes uri = 2; +} + +message ContentIdList { + repeated ContentId contentIds = 1; +} + +enum Type { + EPISODE = 0; +} diff --git a/protocol/proto/player.proto b/protocol/proto/player.proto new file mode 100644 index 00000000..dfe5e5ab --- /dev/null +++ b/protocol/proto/player.proto @@ -0,0 +1,211 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.connectstate; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.connectstate.model"; + +message PlayerState { + int64 timestamp = 1; + string context_uri = 2; + string context_url = 3; + Restrictions context_restrictions = 4; + PlayOrigin play_origin = 5; + ContextIndex index = 6; + ProvidedTrack track = 7; + string playback_id = 8; + double playback_speed = 9; + int64 position_as_of_timestamp = 10; + int64 duration = 11; + bool is_playing = 12; + bool is_paused = 13; + bool is_buffering = 14; + bool is_system_initiated = 15; + ContextPlayerOptions options = 16; + Restrictions restrictions = 17; + Suppressions suppressions = 18; + repeated ProvidedTrack prev_tracks = 19; + repeated ProvidedTrack next_tracks = 20; + map context_metadata = 21; + map page_metadata = 22; + string session_id = 23; + string queue_revision = 24; + int64 position = 25; + string entity_uri = 26; + repeated ProvidedTrack reverse = 27; + repeated ProvidedTrack future = 28; + string audio_stream = 29; + bool is_optional = 30 [deprecated = true]; + int64 bitrate = 31 [deprecated = true]; + PlaybackQuality playback_quality = 32; +} + +message ProvidedTrack { + string uri = 1; + string uid = 2; + map metadata = 3; + repeated string removed = 4; + repeated string blocked = 5; + string provider = 6; + Restrictions restrictions = 7; + string album_uri = 8; + repeated string disallow_reasons = 9; + string artist_uri = 10; + repeated string disallow_undecided = 11; +} + +message ContextIndex { + uint32 page = 1; + uint32 track = 2; +} + +message Restrictions { + repeated string disallow_pausing_reasons = 1; + repeated string disallow_resuming_reasons = 2; + repeated string disallow_seeking_reasons = 3; + repeated string disallow_peeking_prev_reasons = 4; + repeated string disallow_peeking_next_reasons = 5; + repeated string disallow_skipping_prev_reasons = 6; + repeated string disallow_skipping_next_reasons = 7; + repeated string disallow_toggling_repeat_context_reasons = 8; + repeated string disallow_toggling_repeat_track_reasons = 9; + repeated string disallow_toggling_shuffle_reasons = 10; + repeated string disallow_set_queue_reasons = 11; + repeated string disallow_interrupting_playback_reasons = 12; + repeated string disallow_transferring_playback_reasons = 13; + repeated string disallow_remote_control_reasons = 14; + repeated string disallow_inserting_into_next_tracks_reasons = 15; + repeated string disallow_inserting_into_context_tracks_reasons = 16; + repeated string disallow_reordering_in_next_tracks_reasons = 17; + repeated string disallow_reordering_in_context_tracks_reasons = 18; + repeated string disallow_removing_from_next_tracks_reasons = 19; + repeated string disallow_removing_from_context_tracks_reasons = 20; + repeated string disallow_updating_context_reasons = 21; + repeated string disallow_playing_reasons = 22; + repeated string disallow_stopping_reasons = 23; +} + +message PlayOrigin { + string feature_identifier = 1; + string feature_version = 2; + string view_uri = 3; + string external_referrer = 4; + string referrer_identifier = 5; + string device_identifier = 6; + repeated string feature_classes = 7; +} + +message ContextPlayerOptions { + bool shuffling_context = 1; + bool repeating_context = 2; + bool repeating_track = 3; +} + +message Suppressions { + repeated string providers = 1; +} + +message InstrumentationParams { + repeated string interaction_ids = 6; + repeated string page_instance_ids = 7; +} + +message Playback { + int64 timestamp = 1; + int32 position_as_of_timestamp = 2; + double playback_speed = 3; + bool is_paused = 4; + ContextTrack current_track = 5; +} + +message Queue { + repeated ContextTrack tracks = 1; + bool is_playing_queue = 2; +} + +message Session { + PlayOrigin play_origin = 1; + Context context = 2; + string current_uid = 3; + ContextPlayerOptionOverrides option_overrides = 4; + Suppressions suppressions = 5; + InstrumentationParams instrumentation_params = 6; +} + +message TransferState { + ContextPlayerOptions options = 1; + Playback playback = 2; + Session current_session = 3; + Queue queue = 4; +} + +message ContextTrack { + string uri = 1; + string uid = 2; + bytes gid = 3; + map metadata = 4; +} + +message ContextPlayerOptionOverrides { + bool shuffling_context = 1; + bool repeating_context = 2; + bool repeating_track = 3; +} + +message Context { + string uri = 1; + string url = 2; + map metadata = 3; + Restrictions restrictions = 4; + repeated ContextPage pages = 5; + bool loading = 6; +} + +message ContextPage { + string page_url = 1; + string next_page_url = 2; + map metadata = 3; + repeated ContextTrack tracks = 4; + bool loading = 5; +} + +message PlayerQueue { + string revision = 1; + repeated ProvidedTrack next_tracks = 2; + repeated ProvidedTrack prev_tracks = 3; + ProvidedTrack track = 4; +} + +message PlaybackQuality { + BitrateLevel bitrate_level = 1; + BitrateStrategy strategy = 2; + BitrateLevel target_bitrate_level = 3; + bool target_bitrate_available = 4; + HiFiStatus hifi_status = 5; +} + +enum BitrateLevel { + unknown_bitrate_level = 0; + low = 1; + normal = 2; + high = 3; + very_high = 4; + hifi = 5; +} + +enum BitrateStrategy { + unknown_strategy = 0; + best_matching = 1; + backend_advised = 2; + offlined_file = 3; + cached_file = 4; + local_file = 5; +} + +enum HiFiStatus { + none = 0; + off = 1; + on = 2; +} diff --git a/protocol/proto/player_license.proto b/protocol/proto/player_license.proto new file mode 100644 index 00000000..3d0e905d --- /dev/null +++ b/protocol/proto/player_license.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message PlayerLicense { + optional string identifier = 1; +} diff --git a/protocol/proto/player_model.proto b/protocol/proto/player_model.proto new file mode 100644 index 00000000..6856ca0d --- /dev/null +++ b/protocol/proto/player_model.proto @@ -0,0 +1,29 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +import "logging_params.proto"; + +option optimize_for = CODE_SIZE; + +message PlayerModel { + optional bool is_paused = 1; + optional uint64 hash = 2; + optional LoggingParams logging_params = 3; + + optional StartReason start_reason = 4; + enum StartReason { + REMOTE_TRANSFER = 0; + COMEBACK = 1; + PLAY_CONTEXT = 2; + PLAY_SPECIFIC_TRACK = 3; + TRACK_FINISHED = 4; + SKIP_TO_NEXT_TRACK = 5; + SKIP_TO_PREV_TRACK = 6; + ERROR = 7; + IGNORED = 8; + UNKNOWN = 9; + } +} diff --git a/protocol/proto/playlist4_external.proto b/protocol/proto/playlist4_external.proto new file mode 100644 index 00000000..0a5d7084 --- /dev/null +++ b/protocol/proto/playlist4_external.proto @@ -0,0 +1,239 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist4.proto; + +option optimize_for = CODE_SIZE; +option java_outer_classname = "Playlist4ApiProto"; +option java_package = "com.spotify.playlist4.proto"; + +message Item { + required string uri = 1; + optional ItemAttributes attributes = 2; +} + +message MetaItem { + optional bytes revision = 1; + optional ListAttributes attributes = 2; + optional int32 length = 3; + optional int64 timestamp = 4; + optional string owner_username = 5; +} + +message ListItems { + required int32 pos = 1; + required bool truncated = 2; + repeated Item items = 3; + repeated MetaItem meta_items = 4; +} + +message FormatListAttribute { + optional string key = 1; + optional string value = 2; +} + +message PictureSize { + optional string target_name = 1; + optional string url = 2; +} + +message ListAttributes { + optional string name = 1; + optional string description = 2; + optional bytes picture = 3; + optional bool collaborative = 4; + optional string pl3_version = 5; + optional bool deleted_by_owner = 6; + optional string client_id = 10; + optional string format = 11; + repeated FormatListAttribute format_attributes = 12; + repeated PictureSize picture_size = 13; +} + +message ItemAttributes { + optional string added_by = 1; + optional int64 timestamp = 2; + optional int64 seen_at = 9; + optional bool public = 10; + repeated FormatListAttribute format_attributes = 11; + optional bytes item_id = 12; +} + +message Add { + optional int32 from_index = 1; + repeated Item items = 2; + optional bool add_last = 4; + optional bool add_first = 5; +} + +message Rem { + optional int32 from_index = 1; + optional int32 length = 2; + repeated Item items = 3; + optional bool items_as_key = 7; +} + +message Mov { + required int32 from_index = 1; + required int32 length = 2; + required int32 to_index = 3; +} + +message ItemAttributesPartialState { + required ItemAttributes values = 1; + repeated ItemAttributeKind no_value = 2; +} + +message ListAttributesPartialState { + required ListAttributes values = 1; + repeated ListAttributeKind no_value = 2; +} + +message UpdateItemAttributes { + required int32 index = 1; + required ItemAttributesPartialState new_attributes = 2; + optional ItemAttributesPartialState old_attributes = 3; +} + +message UpdateListAttributes { + required ListAttributesPartialState new_attributes = 1; + optional ListAttributesPartialState old_attributes = 2; +} + +message Op { + required Kind kind = 1; + enum Kind { + KIND_UNKNOWN = 0; + ADD = 2; + REM = 3; + MOV = 4; + UPDATE_ITEM_ATTRIBUTES = 5; + UPDATE_LIST_ATTRIBUTES = 6; + } + + optional Add add = 2; + optional Rem rem = 3; + optional Mov mov = 4; + optional UpdateItemAttributes update_item_attributes = 5; + optional UpdateListAttributes update_list_attributes = 6; +} + +message OpList { + repeated Op ops = 1; +} + +message ChangeInfo { + optional string user = 1; + optional int64 timestamp = 2; + optional bool admin = 3; + optional bool undo = 4; + optional bool redo = 5; + optional bool merge = 6; + optional bool compressed = 7; + optional bool migration = 8; + optional int32 split_id = 9; + optional SourceInfo source = 10; +} + +message SourceInfo { + optional Client client = 1; + enum Client { + CLIENT_UNKNOWN = 0; + NATIVE_HERMES = 1; + CLIENT = 2; + PYTHON = 3; + JAVA = 4; + WEBPLAYER = 5; + LIBSPOTIFY = 6; + } + + optional string app = 3; + optional string source = 4; + optional string version = 5; +} + +message Delta { + optional bytes base_version = 1; + repeated Op ops = 2; + optional ChangeInfo info = 4; +} + +message Diff { + required bytes from_revision = 1; + repeated Op ops = 2; + required bytes to_revision = 3; +} + +message ListChanges { + optional bytes base_revision = 1; + repeated Delta deltas = 2; + optional bool want_resulting_revisions = 3; + optional bool want_sync_result = 4; + repeated int64 nonces = 6; +} + +message SelectedListContent { + optional bytes revision = 1; + optional int32 length = 2; + optional ListAttributes attributes = 3; + optional ListItems contents = 5; + optional Diff diff = 6; + optional Diff sync_result = 7; + repeated bytes resulting_revisions = 8; + optional bool multiple_heads = 9; + optional bool up_to_date = 10; + repeated int64 nonces = 14; + optional int64 timestamp = 15; + optional string owner_username = 16; + optional bool abuse_reporting_enabled = 17; +} + +message CreateListReply { + required bytes uri = 1; + optional bytes revision = 2; +} + +message ModifyReply { + required bytes uri = 1; + optional bytes revision = 2; +} + +message SubscribeRequest { + repeated bytes uris = 1; +} + +message UnsubscribeRequest { + repeated bytes uris = 1; +} + +message PlaylistModificationInfo { + optional bytes uri = 1; + optional bytes new_revision = 2; + optional bytes parent_revision = 3; + repeated Op ops = 4; +} + +enum ListAttributeKind { + LIST_UNKNOWN = 0; + LIST_NAME = 1; + LIST_DESCRIPTION = 2; + LIST_PICTURE = 3; + LIST_COLLABORATIVE = 4; + LIST_PL3_VERSION = 5; + LIST_DELETED_BY_OWNER = 6; + LIST_CLIENT_ID = 10; + LIST_FORMAT = 11; + LIST_FORMAT_ATTRIBUTES = 12; + LIST_PICTURE_SIZE = 13; +} + +enum ItemAttributeKind { + ITEM_UNKNOWN = 0; + ITEM_ADDED_BY = 1; + ITEM_TIMESTAMP = 2; + ITEM_SEEN_AT = 9; + ITEM_PUBLIC = 10; + ITEM_FORMAT_ATTRIBUTES = 11; + ITEM_ID = 12; +} diff --git a/protocol/proto/playlist_annotate3.proto b/protocol/proto/playlist_annotate3.proto new file mode 100644 index 00000000..3b6b919f --- /dev/null +++ b/protocol/proto/playlist_annotate3.proto @@ -0,0 +1,41 @@ +// Extracted from: Spotify 1.1.33.569 (Windows) + +syntax = "proto2"; + +package spotify_playlist_annotate3.proto; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist_annotate3.proto"; + +message TakedownRequest { + optional AbuseReportState abuse_report_state = 1; +} + +message AnnotateRequest { + optional string description = 1; + optional string image_uri = 2; +} + +message TranscodedPicture { + optional string target_name = 1; + optional string uri = 2; +} + +message PlaylistAnnotation { + optional string description = 1; + optional string picture = 2; + optional RenderFeatures deprecated_render_features = 3 [default = NORMAL_FEATURES, deprecated = true]; + repeated TranscodedPicture transcoded_picture = 4; + optional bool is_abuse_reporting_enabled = 6 [default = true]; + optional AbuseReportState abuse_report_state = 7 [default = OK]; +} + +enum RenderFeatures { + NORMAL_FEATURES = 1; + EXTENDED_FEATURES = 2; +} + +enum AbuseReportState { + OK = 0; + TAKEN_DOWN = 1; +} diff --git a/protocol/proto/playlist_folder_state.proto b/protocol/proto/playlist_folder_state.proto new file mode 100644 index 00000000..a2d32d71 --- /dev/null +++ b/protocol/proto/playlist_folder_state.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message FolderMetadata { + optional string id = 1; + optional string name = 2; + optional uint32 num_folders = 3; + optional uint32 num_playlists = 4; + optional uint32 num_recursive_folders = 5; + optional uint32 num_recursive_playlists = 6; + optional string link = 7; +} diff --git a/protocol/proto/playlist_get_request.proto b/protocol/proto/playlist_get_request.proto new file mode 100644 index 00000000..7e6dd3f0 --- /dev/null +++ b/protocol/proto/playlist_get_request.proto @@ -0,0 +1,26 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist_esperanto.proto; + +import "policy/playlist_request_decoration_policy.proto"; +import "playlist_query.proto"; +import "playlist_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 PlaylistGetRequest { + string uri = 1; + PlaylistQuery query = 2; + playlist.cosmos.proto.PlaylistRequestDecorationPolicy policy = 3; +} + +message PlaylistGetResponse { + ResponseStatus status = 1; + playlist.cosmos.playlist_request.proto.Response data = 2; +} diff --git a/protocol/proto/playlist_modification_request.proto b/protocol/proto/playlist_modification_request.proto new file mode 100644 index 00000000..2bdb0146 --- /dev/null +++ b/protocol/proto/playlist_modification_request.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist_esperanto.proto; + +import "modification_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 PlaylistModificationRequest { + string uri = 1; + playlist.cosmos.proto.ModificationRequest request = 2; +} + +message PlaylistModificationResponse { + ResponseStatus status = 1; +} diff --git a/protocol/proto/playlist_permission.proto b/protocol/proto/playlist_permission.proto new file mode 100644 index 00000000..babab040 --- /dev/null +++ b/protocol/proto/playlist_permission.proto @@ -0,0 +1,80 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist_permission.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message Permission { + optional bytes revision = 1; + optional PermissionLevel permission_level = 2; +} + +message Capabilities { + optional bool can_view = 1; + optional bool can_administrate_permissions = 2; + repeated PermissionLevel grantable_level = 3; + optional bool can_edit_metadata = 4; + optional bool can_edit_items = 5; +} + +message CapabilitiesMultiRequest { + repeated CapabilitiesRequest request = 1; + optional string fallback_username = 2; + optional string fallback_user_id = 3; + optional string fallback_uri = 4; +} + +message CapabilitiesRequest { + optional string username = 1; + optional string user_id = 2; + optional string uri = 3; + optional bool user_is_owner = 4; +} + +message CapabilitiesMultiResponse { + repeated CapabilitiesResponse response = 1; +} + +message CapabilitiesResponse { + optional ResponseStatus status = 1; + optional Capabilities capabilities = 2; +} + +message SetPermissionLevelRequest { + optional PermissionLevel permission_level = 1; +} + +message SetPermissionResponse { + optional Permission resulting_permission = 1; +} + +message Permissions { + optional Permission base_permission = 1; +} + +message PermissionState { + optional Permissions permissions = 1; + optional Capabilities capabilities = 2; + optional bool is_private = 3; + optional bool is_collaborative = 4; +} + +message PermissionStatePub { + optional PermissionState permission_state = 1; +} + +message ResponseStatus { + optional int32 status_code = 1; + optional string status_message = 2; +} + +enum PermissionLevel { + UNKNOWN = 0; + BLOCKED = 1; + VIEWER = 2; + CONTRIBUTOR = 3; +} diff --git a/protocol/proto/playlist_play_request.proto b/protocol/proto/playlist_play_request.proto new file mode 100644 index 00000000..032b2b2a --- /dev/null +++ b/protocol/proto/playlist_play_request.proto @@ -0,0 +1,31 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist_esperanto.proto; + +import "es_context.proto"; +import "es_play_options.proto"; +import "es_logging_params.proto"; +import "es_prepare_play_options.proto"; +import "es_play_origin.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 PlaylistPlayRequest { + PlaylistQuery playlist_query = 1; + player.esperanto.proto.Context context = 2; + player.esperanto.proto.PlayOptions play_options = 3; + player.esperanto.proto.LoggingParams logging_params = 4; + player.esperanto.proto.PreparePlayOptions prepare_play_options = 5; + player.esperanto.proto.PlayOrigin play_origin = 6; +} + +message PlaylistPlayResponse { + ResponseStatus status = 1; +} diff --git a/protocol/proto/playlist_playback_request.proto b/protocol/proto/playlist_playback_request.proto new file mode 100644 index 00000000..8cd20257 --- /dev/null +++ b/protocol/proto/playlist_playback_request.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +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 PlaybackResponse { + bool success = 1; +} diff --git a/protocol/proto/playlist_playlist_state.proto b/protocol/proto/playlist_playlist_state.proto new file mode 100644 index 00000000..4356fe65 --- /dev/null +++ b/protocol/proto/playlist_playlist_state.proto @@ -0,0 +1,50 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +import "metadata/image_group.proto"; +import "playlist_user_state.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message FormatListAttribute { + optional string key = 1; + optional string value = 2; +} + +message Allows { + optional bool can_insert = 1; + optional bool can_remove = 2; +} + +message PlaylistMetadata { + optional string link = 1; + optional string name = 2; + optional User owner = 3; + optional bool owned_by_self = 4; + optional bool collaborative = 5; + optional uint32 total_length = 6; + optional string description = 7; + optional cosmos_util.proto.ImageGroup pictures = 8; + optional bool followed = 9; + optional bool published = 10; + optional bool browsable_offline = 11; + optional bool description_from_annotate = 12; + optional bool picture_from_annotate = 13; + optional string format_list_type = 14; + repeated FormatListAttribute format_list_attributes = 15; + optional bool can_report_annotation_abuse = 16; + optional bool is_loaded = 17; + optional Allows allows = 18; + optional string load_state = 19; + optional User made_for = 20; +} + +message PlaylistOfflineState { + optional string offline = 1; + optional uint32 sync_progress = 2; +} diff --git a/protocol/proto/playlist_query.proto b/protocol/proto/playlist_query.proto new file mode 100644 index 00000000..afd97614 --- /dev/null +++ b/protocol/proto/playlist_query.proto @@ -0,0 +1,63 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist_esperanto.proto; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.playlist.esperanto.proto"; + +message PlaylistRange { + int32 start = 1; + int32 length = 2; +} + +message PlaylistQuery { + repeated BoolPredicate bool_predicates = 1; + enum BoolPredicate { + NO_FILTER = 0; + AVAILABLE = 1; + AVAILABLE_OFFLINE = 2; + ARTIST_NOT_BANNED = 3; + NOT_BANNED = 4; + NOT_EXPLICIT = 5; + NOT_EPISODE = 6; + } + + string text_filter = 2; + + SortBy sort_by = 3; + enum SortBy { + NO_SORT = 0; + ALBUM_ARTIST_NAME_ASC = 1; + ALBUM_ARTIST_NAME_DESC = 2; + TRACK_NUMBER_ASC = 3; + TRACK_NUMBER_DESC = 4; + DISC_NUMBER_ASC = 5; + DISC_NUMBER_DESC = 6; + ALBUM_NAME_ASC = 7; + ALBUM_NAME_DESC = 8; + ARTIST_NAME_ASC = 9; + ARTIST_NAME_DESC = 10; + NAME_ASC = 11; + NAME_DESC = 12; + ADD_TIME_ASC = 13; + ADD_TIME_DESC = 14; + } + + PlaylistRange range = 4; + int32 update_throttling_ms = 5; + bool group = 6; + PlaylistSourceRestriction source_restriction = 7; + bool show_unavailable = 8; + bool always_show_windowed = 9; + bool load_recommendations = 10; +} + +enum PlaylistSourceRestriction { + NO_RESTRICTION = 0; + RESTRICT_SOURCE_TO_50 = 1; + RESTRICT_SOURCE_TO_500 = 2; +} diff --git a/protocol/proto/playlist_request.proto b/protocol/proto/playlist_request.proto new file mode 100644 index 00000000..cb452f63 --- /dev/null +++ b/protocol/proto/playlist_request.proto @@ -0,0 +1,89 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.playlist_request.proto; + +import "collection/episode_collection_state.proto"; +import "metadata/episode_metadata.proto"; +import "played_state/track_played_state.proto"; +import "played_state/episode_played_state.proto"; +import "sync/episode_sync_state.proto"; +import "metadata/image_group.proto"; +import "on_demand_in_free_reason.proto"; +import "playlist_permission.proto"; +import "playlist_playlist_state.proto"; +import "playlist_track_state.proto"; +import "playlist_user_state.proto"; +import "metadata/track_metadata.proto"; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message Item { + optional string header_field = 1; + optional uint32 add_time = 2; + optional cosmos.proto.User added_by = 3; + optional cosmos_util.proto.TrackMetadata track_metadata = 4; + optional cosmos.proto.TrackCollectionState track_collection_state = 5; + optional cosmos.proto.TrackOfflineState track_offline_state = 6; + optional string row_id = 7; + optional cosmos_util.proto.TrackPlayState track_play_state = 8; + repeated cosmos.proto.FormatListAttribute format_list_attributes = 9; + optional cosmos_util.proto.EpisodeMetadata episode_metadata = 10; + optional cosmos_util.proto.EpisodeSyncState episode_offline_state = 11; + optional cosmos_util.proto.EpisodeCollectionState episode_collection_state = 12; + optional cosmos_util.proto.EpisodePlayState episode_play_state = 13; + optional cosmos_util.proto.ImageGroup display_covers = 14; +} + +message Playlist { + optional cosmos.proto.PlaylistMetadata playlist_metadata = 1; + optional cosmos.proto.PlaylistOfflineState playlist_offline_state = 2; +} + +message RecommendationItem { + optional cosmos_util.proto.TrackMetadata track_metadata = 1; + optional cosmos.proto.TrackCollectionState track_collection_state = 2; + optional cosmos.proto.TrackOfflineState track_offline_state = 3; + optional cosmos_util.proto.TrackPlayState track_play_state = 4; +} + +message Collaborator { + optional cosmos.proto.User user = 1; + optional uint32 number_of_items = 2; + optional uint32 number_of_tracks = 3; + optional uint32 number_of_episodes = 4; + optional bool is_owner = 5; +} + +message Collaborators { + optional uint32 count = 1; + repeated Collaborator collaborator = 2; +} + +message Response { + repeated Item item = 1; + optional Playlist playlist = 2; + optional uint32 unfiltered_length = 3; + optional uint32 unranged_length = 4; + optional uint64 duration = 5; + optional bool loading_contents = 6; + optional uint64 last_modification = 7; + optional uint32 num_followers = 8; + optional bool playable = 9; + repeated RecommendationItem recommendations = 10; + optional bool has_explicit_content = 11; + optional bool contains_spotify_tracks = 12; + optional bool contains_episodes = 13; + optional bool only_contains_explicit = 14; + optional bool contains_audio_episodes = 15; + optional bool contains_tracks = 16; + optional bool is_on_demand_in_free = 17; + optional uint32 number_of_tracks = 18; + optional uint32 number_of_episodes = 19; + optional bool prefer_linear_playback = 20; + optional on_demand_set.proto.OnDemandInFreeReason on_demand_in_free_reason = 21; + optional Collaborators collaborators = 22; + optional playlist_permission.proto.Permission base_permission = 23; +} diff --git a/protocol/proto/playlist_set_base_permission_request.proto b/protocol/proto/playlist_set_base_permission_request.proto new file mode 100644 index 00000000..3e8e1838 --- /dev/null +++ b/protocol/proto/playlist_set_base_permission_request.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist_esperanto.proto; + +import "playlist_set_permission_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 PlaylistSetBasePermissionRequest { + string uri = 1; + playlist.cosmos.proto.SetBasePermissionRequest request = 2; +} + +message PlaylistSetBasePermissionResponse { + ResponseStatus status = 1; + playlist.cosmos.proto.SetBasePermissionResponse response = 2; +} diff --git a/protocol/proto/playlist_set_permission_request.proto b/protocol/proto/playlist_set_permission_request.proto new file mode 100644 index 00000000..a410cc23 --- /dev/null +++ b/protocol/proto/playlist_set_permission_request.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +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 SetBasePermissionRequest { + optional playlist_permission.proto.PermissionLevel permission_level = 1; + optional uint32 timeout_ms = 2; +} + +message SetBasePermissionResponse { + optional playlist_permission.proto.Permission base_permission = 1; +} diff --git a/protocol/proto/playlist_track_state.proto b/protocol/proto/playlist_track_state.proto new file mode 100644 index 00000000..5bd64ae2 --- /dev/null +++ b/protocol/proto/playlist_track_state.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message TrackCollectionState { + optional bool is_in_collection = 1; + optional bool can_add_to_collection = 2; + optional bool is_banned = 3; + optional bool can_ban = 4; +} + +message TrackOfflineState { + optional string offline = 1; +} diff --git a/protocol/proto/playlist_user_state.proto b/protocol/proto/playlist_user_state.proto new file mode 100644 index 00000000..510630ca --- /dev/null +++ b/protocol/proto/playlist_user_state.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message User { + optional string link = 1; + optional string username = 2; + optional string display_name = 3; + optional string image_uri = 4; + optional string thumbnail_uri = 5; +} diff --git a/protocol/proto/playlist_v1_uri.proto b/protocol/proto/playlist_v1_uri.proto new file mode 100644 index 00000000..76c9d797 --- /dev/null +++ b/protocol/proto/playlist_v1_uri.proto @@ -0,0 +1,15 @@ +// 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/plugin.proto b/protocol/proto/plugin.proto new file mode 100644 index 00000000..c0e912ce --- /dev/null +++ b/protocol/proto/plugin.proto @@ -0,0 +1,141 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.offline.proto; + +import "google/protobuf/any.proto"; +import "extension_kind.proto"; +import "resource_type.proto"; + +option optimize_for = CODE_SIZE; + +message PluginRegistry { + repeated Entry plugins = 1; + message Entry { + string id = 1; + repeated LinkType supported_link_types = 2; + ResourceType resource_type = 3; + repeated extendedmetadata.ExtensionKind extension_kinds = 4; + } + + enum LinkType { + EMPTY = 0; + TRACK = 1; + EPISODE = 2; + } +} + +message PluginInit { + string id = 1; +} + +message TargetFormat { + int32 bitrate = 1; +} + +message Metadata { + Header header = 1; + message Header { + int32 status_code = 1; + bool is_empty = 2; + } + + google.protobuf.Any extension_data = 2; +} + +message IdentifyCommand { + Header header = 3; + message Header { + TargetFormat target_format = 1; + } + + repeated Query query = 4; + message Query { + string link = 1; + map metadata = 2; + } +} + +message IdentifyResponse { + map results = 1; + + message Result { + Status status = 1; + enum Status { + UNKNOWN = 0; + MISSING = 1; + COMPLETE = 2; + NOT_APPLICABLE = 3; + } + + int64 estimated_file_size = 2; + } +} + +message DownloadCommand { + string link = 1; + TargetFormat target_format = 2; + map metadata = 3; +} + +message DownloadResponse { + string link = 1; + bool complete = 2; + int64 file_size = 3; + int64 bytes_downloaded = 4; + + Error error = 5; + enum Error { + OK = 0; + TEMPORARY_ERROR = 1; + PERMANENT_ERROR = 2; + DISK_FULL = 3; + } +} + +message StopDownloadCommand { + string link = 1; +} + +message StopDownloadResponse { + +} + +message RemoveCommand { + Header header = 2; + message Header { + + } + + repeated Query query = 3; + message Query { + string link = 1; + } +} + +message RemoveResponse { + +} + +message PluginCommand { + string id = 1; + + oneof command { + IdentifyCommand identify = 2; + DownloadCommand download = 3; + RemoveCommand remove = 4; + StopDownloadCommand stop_download = 5; + } +} + +message PluginResponse { + string id = 1; + + oneof response { + IdentifyResponse identify = 2; + DownloadResponse download = 3; + RemoveResponse remove = 4; + StopDownloadResponse stop_download = 5; + } +} diff --git a/protocol/proto/podcast_ad_segments.proto b/protocol/proto/podcast_ad_segments.proto new file mode 100644 index 00000000..ebff7385 --- /dev/null +++ b/protocol/proto/podcast_ad_segments.proto @@ -0,0 +1,34 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.ads.formats; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "PodcastAdsProto"; +option java_package = "com.spotify.ads.formats.proto"; + +message PodcastAds { + repeated string file_ids = 1; + repeated string manifest_ids = 2; + repeated Segment segments = 3; +} + +message Segment { + Slot slot = 1; + int32 start_ms = 2; + int32 stop_ms = 3; +} + +enum Slot { + UNKNOWN = 0; + PODCAST_PREROLL = 1; + PODCAST_POSTROLL = 2; + PODCAST_MIDROLL_1 = 3; + PODCAST_MIDROLL_2 = 4; + PODCAST_MIDROLL_3 = 5; + PODCAST_MIDROLL_4 = 6; + PODCAST_MIDROLL_5 = 7; +} diff --git a/protocol/proto/podcast_paywalls_cosmos.proto b/protocol/proto/podcast_paywalls_cosmos.proto new file mode 100644 index 00000000..9b818137 --- /dev/null +++ b/protocol/proto/podcast_paywalls_cosmos.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.podcast_paywalls_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message PodcastPaywallsShowSubscriptionRequest { + string show_uri = 1; +} + +message PodcastPaywallsShowSubscriptionResponse { + bool is_user_subscribed = 1; +} diff --git a/protocol/proto/podcast_poll.proto b/protocol/proto/podcast_poll.proto new file mode 100644 index 00000000..60dc04c6 --- /dev/null +++ b/protocol/proto/podcast_poll.proto @@ -0,0 +1,48 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.polls; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "PollMetadataProto"; +option java_package = "com.spotify.podcastcreatorinteractivity.v1"; + +message PodcastPoll { + Poll poll = 1; +} + +message Poll { + int32 id = 1; + string opening_date = 2; + string closing_date = 3; + int32 entity_timestamp_ms = 4; + string entity_uri = 5; + string name = 6; + string question = 7; + PollType type = 8; + repeated PollOption options = 9; + PollStatus status = 10; +} + +message PollOption { + string option = 1; + int32 total_votes = 2; + int32 poll_id = 3; + int32 option_id = 4; +} + +enum PollType { + MULTIPLE_CHOICE = 0; + SINGLE_CHOICE = 1; +} + +enum PollStatus { + DRAFT = 0; + SCHEDULED = 1; + LIVE = 2; + CLOSED = 3; + BLOCKED = 4; +} diff --git a/protocol/proto/podcast_qna.proto b/protocol/proto/podcast_qna.proto new file mode 100644 index 00000000..fca3ba55 --- /dev/null +++ b/protocol/proto/podcast_qna.proto @@ -0,0 +1,33 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.qanda; + +import "google/protobuf/timestamp.proto"; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "QnAMetadataProto"; +option java_package = "com.spotify.podcastcreatorinteractivity.v1"; + +message PodcastQna { + Prompt prompt = 1; +} + +message Prompt { + int32 id = 1; + google.protobuf.Timestamp opening_date = 2; + google.protobuf.Timestamp closing_date = 3; + string text = 4; + QAndAStatus status = 5; +} + +enum QAndAStatus { + DRAFT = 0; + SCHEDULED = 1; + LIVE = 2; + CLOSED = 3; + DELETED = 4; +} diff --git a/protocol/proto/podcast_segments.proto b/protocol/proto/podcast_segments.proto new file mode 100644 index 00000000..52a075f3 --- /dev/null +++ b/protocol/proto/podcast_segments.proto @@ -0,0 +1,47 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.podcast_segments; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "PodcastSegmentsProto"; +option java_package = "com.spotify.podcastsegments.proto"; + +message PodcastSegments { + string episode_uri = 1; + repeated PlaybackSegment playback_segments = 2; + repeated EmbeddedSegment embedded_segments = 3; + bool can_upsell = 4; + string album_mosaic_uri = 5; + repeated string artists = 6; + int32 duration_ms = 7; +} + +message PlaybackSegment { + string uri = 1; + int32 start_ms = 2; + int32 stop_ms = 3; + int32 duration_ms = 4; + SegmentType type = 5; + string title = 6; + string subtitle = 7; + string image_url = 8; + string action_url = 9; + bool is_abridged = 10; +} + +message EmbeddedSegment { + string uri = 1; + int32 absolute_start_ms = 2; + int32 absolute_stop_ms = 3; +} + +enum SegmentType { + UNKNOWN = 0; + TALK = 1; + MUSIC = 2; + UPSELL = 3; +} diff --git a/protocol/proto/podcast_segments_cosmos_request.proto b/protocol/proto/podcast_segments_cosmos_request.proto new file mode 100644 index 00000000..1d5a51f4 --- /dev/null +++ b/protocol/proto/podcast_segments_cosmos_request.proto @@ -0,0 +1,37 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.podcast_segments.cosmos.proto; + +import "policy/album_decoration_policy.proto"; +import "policy/artist_decoration_policy.proto"; +import "policy/episode_decoration_policy.proto"; +import "policy/track_decoration_policy.proto"; +import "policy/show_decoration_policy.proto"; + +option optimize_for = CODE_SIZE; + +message SegmentsRequest { + repeated string episode_uris = 1; + TrackDecorationPolicy track_decoration_policy = 2; + SegmentsPolicy segments_policy = 3; + EpisodeDecorationPolicy episode_decoration_policy = 4; +} + +message TrackDecorationPolicy { + cosmos_util.proto.TrackDecorationPolicy track_policy = 1; + cosmos_util.proto.ArtistDecorationPolicy artists_policy = 2; + cosmos_util.proto.AlbumDecorationPolicy album_policy = 3; + cosmos_util.proto.ArtistDecorationPolicy album_artist_policy = 4; +} + +message SegmentsPolicy { + bool playback = 1; + bool embedded = 2; +} + +message EpisodeDecorationPolicy { + cosmos_util.proto.EpisodeDecorationPolicy episode_policy = 1; + cosmos_util.proto.ShowDecorationPolicy show_decoration_policy = 2; +} diff --git a/protocol/proto/podcast_segments_cosmos_response.proto b/protocol/proto/podcast_segments_cosmos_response.proto new file mode 100644 index 00000000..a80f7270 --- /dev/null +++ b/protocol/proto/podcast_segments_cosmos_response.proto @@ -0,0 +1,39 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.podcast_segments.cosmos.proto; + +import "metadata/episode_metadata.proto"; +import "podcast_segments.proto"; +import "metadata/track_metadata.proto"; + +option optimize_for = CODE_SIZE; + +message SegmentsResponse { + bool success = 1; + repeated EpisodeSegments episode_segments = 2; +} + +message EpisodeSegments { + string episode_uri = 1; + repeated DecoratedSegment segments = 2; + bool can_upsell = 3; + string album_mosaic_uri = 4; + repeated string artists = 5; + int32 duration_ms = 6; +} + +message DecoratedSegment { + string uri = 1; + int32 start_ms = 2; + int32 stop_ms = 3; + cosmos_util.proto.TrackMetadata track_metadata = 4; + SegmentType type = 5; + string title = 6; + string subtitle = 7; + string image_url = 8; + string action_url = 9; + cosmos_util.proto.EpisodeMetadata episode_metadata = 10; + bool is_abridged = 11; +} diff --git a/protocol/proto/podcast_subscription.proto b/protocol/proto/podcast_subscription.proto new file mode 100644 index 00000000..52b7f8f3 --- /dev/null +++ b/protocol/proto/podcast_subscription.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.podcast_paywalls; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "PodcastSubscriptionProto"; +option java_package = "com.spotify.podcastsubscription.proto"; + +message PodcastSubscription { + bool is_paywalled = 1; + bool is_user_subscribed = 2; + + UserExplanation user_explanation = 3; + enum UserExplanation { + SUBSCRIPTION_DIALOG = 0; + NONE = 1; + } +} diff --git a/protocol/proto/podcast_virality.proto b/protocol/proto/podcast_virality.proto new file mode 100644 index 00000000..902dca90 --- /dev/null +++ b/protocol/proto/podcast_virality.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.podcastvirality.v1; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "PodcastViralityProto"; +option java_package = "com.spotify.podcastvirality.proto.v1"; + +message PodcastVirality { + bool is_viral = 1; +} diff --git a/protocol/proto/podcastextensions.proto b/protocol/proto/podcastextensions.proto new file mode 100644 index 00000000..4c85e396 --- /dev/null +++ b/protocol/proto/podcastextensions.proto @@ -0,0 +1,29 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.podcast.extensions; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "PodcastExtensionsProto"; +option java_package = "com.spotify.podcastextensions.proto"; + +message PodcastTopics { + repeated PodcastTopic topics = 1; +} + +message PodcastTopic { + string uri = 1; + string title = 2; +} + +message PodcastHtmlDescription { + Header header = 1; + message Header { + + } + + string html_description = 2; +} diff --git a/protocol/proto/policy/album_decoration_policy.proto b/protocol/proto/policy/album_decoration_policy.proto new file mode 100644 index 00000000..a20cf324 --- /dev/null +++ b/protocol/proto/policy/album_decoration_policy.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.policy.proto"; + +message AlbumDecorationPolicy { + bool link = 1; + bool name = 2; + bool copyrights = 3; + bool covers = 4; + bool year = 5; + bool num_discs = 6; + bool num_tracks = 7; + bool playability = 8; + bool is_premium_only = 9; +} diff --git a/protocol/proto/policy/artist_decoration_policy.proto b/protocol/proto/policy/artist_decoration_policy.proto new file mode 100644 index 00000000..f8d8b2cb --- /dev/null +++ b/protocol/proto/policy/artist_decoration_policy.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.policy.proto"; + +message ArtistDecorationPolicy { + bool link = 1; + bool name = 2; + bool is_various_artists = 3; + bool portraits = 4; +} diff --git a/protocol/proto/policy/episode_decoration_policy.proto b/protocol/proto/policy/episode_decoration_policy.proto new file mode 100644 index 00000000..77489834 --- /dev/null +++ b/protocol/proto/policy/episode_decoration_policy.proto @@ -0,0 +1,58 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.policy.proto"; + +message EpisodeDecorationPolicy { + bool link = 1; + bool length = 2; + bool name = 3; + bool manifest_id = 4; + bool preview_id = 5; + bool preview_manifest_id = 6; + bool description = 7; + bool publish_date = 8; + bool covers = 9; + bool freeze_frames = 10; + bool language = 11; + bool available = 12; + bool media_type_enum = 13; + bool number = 14; + bool backgroundable = 15; + bool is_explicit = 16; + bool type = 17; + bool is_music_and_talk = 18; + PodcastSegmentsPolicy podcast_segments = 19; + bool podcast_subscription = 20; +} + +message EpisodeCollectionDecorationPolicy { + bool is_following_show = 1; + bool is_in_listen_later = 2; + bool is_new = 3; +} + +message EpisodeSyncDecorationPolicy { + bool offline = 1; + bool sync_progress = 2; +} + +message EpisodePlayedStateDecorationPolicy { + bool time_left = 1; + bool is_played = 2; + bool playable = 3; + bool playability_restriction = 4; +} + +message PodcastSegmentsPolicy { + bool playback_segments = 1; + bool embedded_segments = 2; + bool can_upsell = 3; + bool album_mosaic_uri = 4; + bool artists = 5; +} diff --git a/protocol/proto/policy/folder_decoration_policy.proto b/protocol/proto/policy/folder_decoration_policy.proto new file mode 100644 index 00000000..0d47e4d6 --- /dev/null +++ b/protocol/proto/policy/folder_decoration_policy.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist.cosmos.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.policy.proto"; + +message FolderDecorationPolicy { + bool row_id = 1; + bool id = 2; + bool link = 3; + bool name = 4; + bool folders = 5; + bool playlists = 6; + bool recursive_folders = 7; + bool recursive_playlists = 8; + bool rows = 9; +} diff --git a/protocol/proto/policy/playlist_album_decoration_policy.proto b/protocol/proto/policy/playlist_album_decoration_policy.proto new file mode 100644 index 00000000..01537c78 --- /dev/null +++ b/protocol/proto/policy/playlist_album_decoration_policy.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist.cosmos.proto; + +import "policy/album_decoration_policy.proto"; +import "policy/artist_decoration_policy.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.policy.proto"; + +message PlaylistAlbumDecorationPolicy { + cosmos_util.proto.AlbumDecorationPolicy album = 1; + cosmos_util.proto.ArtistDecorationPolicy artist = 2; +} diff --git a/protocol/proto/policy/playlist_decoration_policy.proto b/protocol/proto/policy/playlist_decoration_policy.proto new file mode 100644 index 00000000..9975279c --- /dev/null +++ b/protocol/proto/policy/playlist_decoration_policy.proto @@ -0,0 +1,60 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist.cosmos.proto; + +import "policy/user_decoration_policy.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.policy.proto"; + +message PlaylistAllowsDecorationPolicy { + bool insert = 1; + bool remove = 2; +} + +message PlaylistDecorationPolicy { + bool row_id = 1; + bool link = 2; + bool name = 3; + bool load_state = 4; + bool loaded = 5; + bool collaborative = 6; + bool length = 7; + bool last_modification = 8; + bool total_length = 9; + bool duration = 10; + bool description = 11; + bool picture = 12; + bool playable = 13; + bool description_from_annotate = 14; + bool picture_from_annotate = 15; + bool can_report_annotation_abuse = 16; + bool followed = 17; + bool followers = 18; + bool owned_by_self = 19; + bool offline = 20; + bool sync_progress = 21; + bool published = 22; + bool browsable_offline = 23; + bool format_list_type = 24; + bool format_list_attributes = 25; + bool has_explicit_content = 26; + bool contains_spotify_tracks = 27; + bool contains_tracks = 28; + bool contains_episodes = 29; + bool contains_audio_episodes = 30; + bool only_contains_explicit = 31; + bool is_on_demand_in_free = 32; + UserDecorationPolicy owner = 33; + UserDecorationPolicy made_for = 34; + PlaylistAllowsDecorationPolicy allows = 35; + bool number_of_episodes = 36; + bool number_of_tracks = 37; + bool prefer_linear_playback = 38; + bool on_demand_in_free_reason = 39; + CollaboratingUsersDecorationPolicy collaborating_users = 40; + bool base_permission = 41; +} diff --git a/protocol/proto/policy/playlist_episode_decoration_policy.proto b/protocol/proto/policy/playlist_episode_decoration_policy.proto new file mode 100644 index 00000000..4e038944 --- /dev/null +++ b/protocol/proto/policy/playlist_episode_decoration_policy.proto @@ -0,0 +1,25 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist.cosmos.proto; + +import "policy/episode_decoration_policy.proto"; +import "policy/show_decoration_policy.proto"; +import "policy/user_decoration_policy.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.policy.proto"; + +message PlaylistEpisodeDecorationPolicy { + cosmos_util.proto.EpisodeDecorationPolicy episode = 1; + bool row_id = 2; + bool add_time = 3; + bool format_list_attributes = 4; + cosmos_util.proto.EpisodeCollectionDecorationPolicy collection = 5; + cosmos_util.proto.EpisodeSyncDecorationPolicy sync = 6; + cosmos_util.proto.EpisodePlayedStateDecorationPolicy played_state = 7; + UserDecorationPolicy added_by = 8; + cosmos_util.proto.ShowDecorationPolicy show = 9; +} diff --git a/protocol/proto/policy/playlist_request_decoration_policy.proto b/protocol/proto/policy/playlist_request_decoration_policy.proto new file mode 100644 index 00000000..a1663d28 --- /dev/null +++ b/protocol/proto/policy/playlist_request_decoration_policy.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist.cosmos.proto; + +import "policy/playlist_decoration_policy.proto"; +import "policy/playlist_episode_decoration_policy.proto"; +import "policy/playlist_track_decoration_policy.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.policy.proto"; + +message PlaylistRequestDecorationPolicy { + PlaylistDecorationPolicy playlist = 1; + PlaylistTrackDecorationPolicy track = 2; + PlaylistEpisodeDecorationPolicy episode = 3; +} diff --git a/protocol/proto/policy/playlist_track_decoration_policy.proto b/protocol/proto/policy/playlist_track_decoration_policy.proto new file mode 100644 index 00000000..97eb0187 --- /dev/null +++ b/protocol/proto/policy/playlist_track_decoration_policy.proto @@ -0,0 +1,31 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist.cosmos.proto; + +import "policy/artist_decoration_policy.proto"; +import "policy/track_decoration_policy.proto"; +import "policy/playlist_album_decoration_policy.proto"; +import "policy/user_decoration_policy.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.policy.proto"; + +message PlaylistTrackDecorationPolicy { + cosmos_util.proto.TrackDecorationPolicy track = 1; + bool row_id = 2; + bool add_time = 3; + bool in_collection = 4; + bool can_add_to_collection = 5; + bool is_banned = 6; + bool can_ban = 7; + bool local_file = 8; + bool offline = 9; + bool format_list_attributes = 10; + bool display_covers = 11; + UserDecorationPolicy added_by = 12; + PlaylistAlbumDecorationPolicy album = 13; + cosmos_util.proto.ArtistDecorationPolicy artist = 14; +} diff --git a/protocol/proto/policy/rootlist_folder_decoration_policy.proto b/protocol/proto/policy/rootlist_folder_decoration_policy.proto new file mode 100644 index 00000000..f93888b4 --- /dev/null +++ b/protocol/proto/policy/rootlist_folder_decoration_policy.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +import "policy/folder_decoration_policy.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.policy.proto"; + +message RootlistFolderDecorationPolicy { + optional bool add_time = 1; + optional FolderDecorationPolicy folder = 2; + optional bool group_label = 3; +} diff --git a/protocol/proto/policy/rootlist_playlist_decoration_policy.proto b/protocol/proto/policy/rootlist_playlist_decoration_policy.proto new file mode 100644 index 00000000..9e8446ab --- /dev/null +++ b/protocol/proto/policy/rootlist_playlist_decoration_policy.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +import "policy/playlist_decoration_policy.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.policy.proto"; + +message RootlistPlaylistDecorationPolicy { + optional bool add_time = 1; + optional PlaylistDecorationPolicy playlist = 2; + optional bool group_label = 3; +} diff --git a/protocol/proto/policy/rootlist_request_decoration_policy.proto b/protocol/proto/policy/rootlist_request_decoration_policy.proto new file mode 100644 index 00000000..ebad00ca --- /dev/null +++ b/protocol/proto/policy/rootlist_request_decoration_policy.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.proto; + +import "policy/rootlist_folder_decoration_policy.proto"; +import "policy/rootlist_playlist_decoration_policy.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.policy.proto"; + +message RootlistRequestDecorationPolicy { + optional bool unfiltered_length = 1; + optional bool unranged_length = 2; + optional bool is_loading_contents = 3; + optional RootlistPlaylistDecorationPolicy playlist = 4; + optional RootlistFolderDecorationPolicy folder = 5; +} diff --git a/protocol/proto/policy/show_decoration_policy.proto b/protocol/proto/policy/show_decoration_policy.proto new file mode 100644 index 00000000..02ae2f3e --- /dev/null +++ b/protocol/proto/policy/show_decoration_policy.proto @@ -0,0 +1,31 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.policy.proto"; + +message ShowDecorationPolicy { + bool link = 1; + bool name = 2; + bool description = 3; + bool popularity = 4; + bool publisher = 5; + bool language = 6; + bool is_explicit = 7; + bool covers = 8; + bool num_episodes = 9; + bool consumption_order = 10; + bool media_type_enum = 11; + bool copyrights = 12; + bool trailer_uri = 13; + bool is_music_and_talk = 14; + bool access_info = 15; +} + +message ShowPlayedStateDecorationPolicy { + bool latest_played_episode_link = 1; +} diff --git a/protocol/proto/policy/track_decoration_policy.proto b/protocol/proto/policy/track_decoration_policy.proto new file mode 100644 index 00000000..45162008 --- /dev/null +++ b/protocol/proto/policy/track_decoration_policy.proto @@ -0,0 +1,36 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.policy.proto"; + +message TrackDecorationPolicy { + bool has_lyrics = 1; + bool link = 2; + bool name = 3; + bool length = 4; + bool playable = 5; + bool is_available_in_metadata_catalogue = 6; + bool locally_playable = 7; + bool playable_local_track = 8; + bool disc_number = 9; + bool track_number = 10; + bool is_explicit = 11; + bool preview_id = 12; + bool is_local = 13; + bool is_premium_only = 14; + bool playable_track_link = 15; + bool popularity = 16; + bool is_19_plus_only = 17; + bool track_descriptors = 18; +} + +message TrackPlayedStateDecorationPolicy { + bool playable = 1; + bool is_currently_playable = 2; + bool playability_restriction = 3; +} diff --git a/protocol/proto/policy/user_decoration_policy.proto b/protocol/proto/policy/user_decoration_policy.proto new file mode 100644 index 00000000..4f72e974 --- /dev/null +++ b/protocol/proto/policy/user_decoration_policy.proto @@ -0,0 +1,31 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist.cosmos.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.policy.proto"; + +message UserDecorationPolicy { + bool username = 1; + bool link = 2; + bool name = 3; + bool image = 4; + bool thumbnail = 5; +} + +message CollaboratorPolicy { + UserDecorationPolicy user = 1; + bool number_of_items = 2; + bool number_of_tracks = 3; + bool number_of_episodes = 4; + bool is_owner = 5; +} + +message CollaboratingUsersDecorationPolicy { + bool count = 1; + int32 limit = 2; + CollaboratorPolicy collaborator = 3; +} diff --git a/protocol/proto/popcount.proto b/protocol/proto/popcount.proto deleted file mode 100644 index 7a0bac84..00000000 --- a/protocol/proto/popcount.proto +++ /dev/null @@ -1,13 +0,0 @@ -syntax = "proto2"; - -message PopcountRequest { -} - -message PopcountResult { - optional sint64 count = 0x1; - optional bool truncated = 0x2; - repeated string user = 0x3; - repeated sint64 subscriptionTimestamps = 0x4; - repeated sint64 insertionTimestamps = 0x5; -} - diff --git a/protocol/proto/popcount2_external.proto b/protocol/proto/popcount2_external.proto new file mode 100644 index 00000000..069cdcfd --- /dev/null +++ b/protocol/proto/popcount2_external.proto @@ -0,0 +1,30 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.popcount2.proto; + +option optimize_for = CODE_SIZE; + +message PopcountRequest { + +} + +message PopcountResult { + optional sint64 count = 1; + optional bool truncated = 2; + repeated string user = 3; +} + +message PopcountUserUpdate { + optional string user = 1; + optional sint64 timestamp = 2; + optional bool added = 3; +} + +message PopcountUpdate { + repeated PopcountUserUpdate updates = 1; + optional sint64 common_timestamp = 2; + optional sint64 remove_older_than_timestamp = 3; + optional bool verify_counter = 4; +} diff --git a/protocol/proto/prepare_play_options.proto b/protocol/proto/prepare_play_options.proto new file mode 100644 index 00000000..cfaeab14 --- /dev/null +++ b/protocol/proto/prepare_play_options.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +import "context_player_options.proto"; +import "player_license.proto"; + +option optimize_for = CODE_SIZE; + +message PreparePlayOptions { + optional ContextPlayerOptionOverrides player_options_override = 1; + optional PlayerLicense license = 2; + map configuration_override = 3; +} diff --git a/protocol/proto/presence.proto b/protocol/proto/presence.proto deleted file mode 100644 index 5e9be377..00000000 --- a/protocol/proto/presence.proto +++ /dev/null @@ -1,94 +0,0 @@ -syntax = "proto2"; - -message PlaylistPublishedState { - optional string uri = 0x1; - optional int64 timestamp = 0x2; -} - -message PlaylistTrackAddedState { - optional string playlist_uri = 0x1; - optional string track_uri = 0x2; - optional int64 timestamp = 0x3; -} - -message TrackFinishedPlayingState { - optional string uri = 0x1; - optional string context_uri = 0x2; - optional int64 timestamp = 0x3; - optional string referrer_uri = 0x4; -} - -message FavoriteAppAddedState { - optional string app_uri = 0x1; - optional int64 timestamp = 0x2; -} - -message TrackStartedPlayingState { - optional string uri = 0x1; - optional string context_uri = 0x2; - optional int64 timestamp = 0x3; - optional string referrer_uri = 0x4; -} - -message UriSharedState { - optional string uri = 0x1; - optional string message = 0x2; - optional int64 timestamp = 0x3; -} - -message ArtistFollowedState { - optional string uri = 0x1; - optional string artist_name = 0x2; - optional string artist_cover_uri = 0x3; - optional int64 timestamp = 0x4; -} - -message DeviceInformation { - optional string os = 0x1; - optional string type = 0x2; -} - -message GenericPresenceState { - optional int32 type = 0x1; - optional int64 timestamp = 0x2; - optional string item_uri = 0x3; - optional string item_name = 0x4; - optional string item_image = 0x5; - optional string context_uri = 0x6; - optional string context_name = 0x7; - optional string context_image = 0x8; - optional string referrer_uri = 0x9; - optional string referrer_name = 0xa; - optional string referrer_image = 0xb; - optional string message = 0xc; - optional DeviceInformation device_information = 0xd; -} - -message State { - optional int64 timestamp = 0x1; - optional Type type = 0x2; - enum Type { - PLAYLIST_PUBLISHED = 0x1; - PLAYLIST_TRACK_ADDED = 0x2; - TRACK_FINISHED_PLAYING = 0x3; - FAVORITE_APP_ADDED = 0x4; - TRACK_STARTED_PLAYING = 0x5; - URI_SHARED = 0x6; - ARTIST_FOLLOWED = 0x7; - GENERIC = 0xb; - } - optional string uri = 0x3; - optional PlaylistPublishedState playlist_published = 0x4; - optional PlaylistTrackAddedState playlist_track_added = 0x5; - optional TrackFinishedPlayingState track_finished_playing = 0x6; - optional FavoriteAppAddedState favorite_app_added = 0x7; - optional TrackStartedPlayingState track_started_playing = 0x8; - optional UriSharedState uri_shared = 0x9; - optional ArtistFollowedState artist_followed = 0xa; - optional GenericPresenceState generic = 0xb; -} - -message StateList { - repeated State states = 0x1; -} - diff --git a/protocol/proto/profile_cache.proto b/protocol/proto/profile_cache.proto new file mode 100644 index 00000000..8162612f --- /dev/null +++ b/protocol/proto/profile_cache.proto @@ -0,0 +1,19 @@ +// 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_cosmos.proto b/protocol/proto/profile_cosmos.proto new file mode 100644 index 00000000..c6c945db --- /dev/null +++ b/protocol/proto/profile_cosmos.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.profile_cosmos.proto; + +import "identity.proto"; + +option optimize_for = CODE_SIZE; + +message GetProfilesRequest { + repeated string usernames = 1; +} + +message GetProfilesResponse { + repeated identity.v3.UserProfile profiles = 1; +} + +message ChangeDisplayNameRequest { + string username = 1; + string display_name = 2; +} diff --git a/protocol/proto/property_definition.proto b/protocol/proto/property_definition.proto new file mode 100644 index 00000000..4552c1b2 --- /dev/null +++ b/protocol/proto/property_definition.proto @@ -0,0 +1,44 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.remote_config.ucs.proto; + +option optimize_for = CODE_SIZE; + +message PropertyDefinition { + Identifier id = 1; + message Identifier { + string scope = 1; + string name = 2; + } + + Metadata metadata = 4; + message Metadata { + string component_id = 1; + string description = 2; + } + + oneof specification { + BoolSpec bool_spec = 5; + IntSpec int_spec = 6; + EnumSpec enum_spec = 7; + } + + reserved 2, "hash"; + + message BoolSpec { + bool default = 1; + } + + message IntSpec { + int32 default = 1; + int32 lower = 2; + int32 upper = 3; + } + + message EnumSpec { + string default = 1; + repeated string values = 2; + } +} diff --git a/protocol/proto/protobuf_delta.proto b/protocol/proto/protobuf_delta.proto new file mode 100644 index 00000000..c0a89fec --- /dev/null +++ b/protocol/proto/protobuf_delta.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.protobuf_deltas.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message Delta { + required Type type = 1; + enum Type { + DELETE = 0; + INSERT = 1; + } + + required uint32 index = 2; + required uint32 length = 3; +} diff --git a/protocol/proto/queue.proto b/protocol/proto/queue.proto new file mode 100644 index 00000000..24b45b7c --- /dev/null +++ b/protocol/proto/queue.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto.transfer; + +import "context_track.proto"; + +option optimize_for = CODE_SIZE; + +message Queue { + repeated ContextTrack tracks = 1; + optional bool is_playing_queue = 2; +} diff --git a/protocol/proto/radio.proto b/protocol/proto/radio.proto deleted file mode 100644 index 7a8f3bde..00000000 --- a/protocol/proto/radio.proto +++ /dev/null @@ -1,58 +0,0 @@ -syntax = "proto2"; - -message RadioRequest { - repeated string uris = 0x1; - optional int32 salt = 0x2; - optional int32 length = 0x4; - optional string stationId = 0x5; - repeated string lastTracks = 0x6; -} - -message MultiSeedRequest { - repeated string uris = 0x1; -} - -message Feedback { - optional string uri = 0x1; - optional string type = 0x2; - optional double timestamp = 0x3; -} - -message Tracks { - repeated string gids = 0x1; - optional string source = 0x2; - optional string identity = 0x3; - repeated string tokens = 0x4; - repeated Feedback feedback = 0x5; -} - -message Station { - optional string id = 0x1; - optional string title = 0x2; - optional string titleUri = 0x3; - optional string subtitle = 0x4; - optional string subtitleUri = 0x5; - optional string imageUri = 0x6; - optional double lastListen = 0x7; - repeated string seeds = 0x8; - optional int32 thumbsUp = 0x9; - optional int32 thumbsDown = 0xa; -} - -message Rules { - optional string js = 0x1; -} - -message StationResponse { - optional Station station = 0x1; - repeated Feedback feedback = 0x2; -} - -message StationList { - repeated Station stations = 0x1; -} - -message LikedPlaylist { - optional string uri = 0x1; -} - diff --git a/protocol/proto/rc_dummy_property_resolved.proto b/protocol/proto/rc_dummy_property_resolved.proto new file mode 100644 index 00000000..9c5e2aaf --- /dev/null +++ b/protocol/proto/rc_dummy_property_resolved.proto @@ -0,0 +1,12 @@ +// 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 new file mode 100644 index 00000000..ed8405c2 --- /dev/null +++ b/protocol/proto/rcs.proto @@ -0,0 +1,107 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.remote_config.proto; + +option optimize_for = CODE_SIZE; + +message GranularConfiguration { + repeated AssignedPropertyValue properties = 1; + message AssignedPropertyValue { + Platform platform = 7; + string client_id = 4; + string component_id = 5; + int64 groupId = 8; + string name = 6; + + oneof structured_value { + BoolValue bool_value = 1; + IntValue int_value = 2; + EnumValue enum_value = 3; + } + + message BoolValue { + bool value = 1; + } + + message IntValue { + int32 value = 1; + } + + message EnumValue { + string value = 1; + } + } + + int64 rcs_fetch_time = 2; + string configuration_assignment_id = 3; +} + +message PolicyGroupId { + int64 policy_id = 1; + int64 policy_group_id = 2; +} + +message ClientPropertySet { + string client_id = 1; + string version = 2; + repeated PropertyDefinition properties = 5; + + repeated ComponentInfo component_infos = 6; + message ComponentInfo { + string name = 3; + + reserved 1, 2, "owner", "tags"; + } + + string property_set_key = 7; + + PublisherInfo publisherInfo = 8; + message PublisherInfo { + string published_for_client_version = 1; + int64 published_at = 2; + } +} + +message PropertyDefinition { + string description = 2; + string component_id = 3; + Platform platform = 8; + + oneof identifier { + string id = 9; + string name = 7; + } + + oneof spec { + BoolSpec bool_spec = 4; + IntSpec int_spec = 5; + EnumSpec enum_spec = 6; + } + + reserved 1; + + message BoolSpec { + bool default = 1; + } + + message IntSpec { + int32 default = 1; + int32 lower = 2; + int32 upper = 3; + } + + message EnumSpec { + string default = 1; + repeated string values = 2; + } +} + +enum Platform { + UNKNOWN_PLATFORM = 0; + ANDROID_PLATFORM = 1; + BACKEND_PLATFORM = 2; + IOS_PLATFORM = 3; + WEB_PLATFORM = 4; +} diff --git a/protocol/proto/recently_played.proto b/protocol/proto/recently_played.proto new file mode 100644 index 00000000..fd22fdd9 --- /dev/null +++ b/protocol/proto/recently_played.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.recently_played.proto; + +option optimize_for = CODE_SIZE; + +message Item { + string link = 1; + int64 timestamp = 2; + bool hidden = 3; +} diff --git a/protocol/proto/recently_played_backend.proto b/protocol/proto/recently_played_backend.proto new file mode 100644 index 00000000..fa137288 --- /dev/null +++ b/protocol/proto/recently_played_backend.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.recently_played_backend.proto; + +option optimize_for = CODE_SIZE; + +message Context { + optional string uri = 1; + optional int64 lastPlayedTime = 2; +} + +message RecentlyPlayed { + repeated Context contexts = 1; + optional int32 offset = 2; + optional int32 total = 3; +} diff --git a/protocol/proto/record_id.proto b/protocol/proto/record_id.proto new file mode 100644 index 00000000..54fa24a3 --- /dev/null +++ b/protocol/proto/record_id.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message RecordId { + int64 value = 1; +} diff --git a/protocol/proto/remote.proto b/protocol/proto/remote.proto new file mode 100644 index 00000000..a81c1c0f --- /dev/null +++ b/protocol/proto/remote.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.shuffle.remote; + +option optimize_for = CODE_SIZE; + +message ServiceRequest { + repeated Track tracks = 1; + message Track { + required string uri = 1; + required string uid = 2; + } +} + +message ServiceResponse { + repeated uint32 order = 1; +} diff --git a/protocol/proto/repeating_track_node.proto b/protocol/proto/repeating_track_node.proto new file mode 100644 index 00000000..d4691cd2 --- /dev/null +++ b/protocol/proto/repeating_track_node.proto @@ -0,0 +1,15 @@ +// 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 RepeatingTrackNode { + optional TrackInstance instance = 1; + optional TrackInstantiator instantiator = 2; +} diff --git a/protocol/proto/request_failure.proto b/protocol/proto/request_failure.proto new file mode 100644 index 00000000..10deb1be --- /dev/null +++ b/protocol/proto/request_failure.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.image.proto; + +option optimize_for = CODE_SIZE; + +message RequestFailure { + optional string request = 1; + optional string source = 2; + optional string error = 3; + optional int64 result = 4; +} diff --git a/protocol/proto/resolve.proto b/protocol/proto/resolve.proto new file mode 100644 index 00000000..5f2cd9b8 --- /dev/null +++ b/protocol/proto/resolve.proto @@ -0,0 +1,116 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.remote_config.ucs.proto; + +import "property_definition.proto"; + +option optimize_for = CODE_SIZE; + +message ResolveRequest { + string property_set_id = 1; + Fetch fetch_type = 2; + Context context = 11; + + oneof resolution_context { + BackendContext backend_context = 12 [deprecated = true]; + } + + reserved 4, 5, "custom_context", "projection"; +} + +message ResolveResponse { + Configuration configuration = 1; +} + +message Configuration { + string configuration_assignment_id = 1; + int64 fetch_time_millis = 2; + + repeated AssignedValue assigned_values = 3; + message AssignedValue { + PropertyDefinition.Identifier property_id = 1; + + Metadata metadata = 2; + message Metadata { + int64 policy_id = 1; + string external_realm = 2; + int64 external_realm_id = 3; + } + + oneof structured_value { + BoolValue bool_value = 3; + IntValue int_value = 4; + EnumValue enum_value = 5; + } + + message BoolValue { + bool value = 1; + } + + message IntValue { + int32 value = 1; + } + + message EnumValue { + string value = 1; + } + } +} + +message Fetch { + Type type = 1; + enum Type { + BLOCKING = 0; + BACKGROUND_SYNC = 1; + ASYNC = 2; + PUSH_INITIATED = 3; + RECONNECT = 4; + } +} + +message Context { + repeated ContextEntry context = 1; + message ContextEntry { + string value = 10; + + oneof context { + DynamicContext.KnownContext known_context = 1; + } + } +} + +message BackendContext { + string system = 1 [deprecated = true]; + string service_name = 2 [deprecated = true]; + + StaticContext static_context = 3; + message StaticContext { + string system = 1; + string service_name = 2; + } + + DynamicContext dynamic_context = 4; + + SurfaceMetadata surface_metadata = 10; + message SurfaceMetadata { + string backend_sdk_version = 1; + } +} + +message DynamicContext { + repeated ContextDefinition context_definition = 1; + message ContextDefinition { + oneof context { + KnownContext known_context = 1; + } + } + + enum KnownContext { + KNOWN_CONTEXT_INVALID = 0; + KNOWN_CONTEXT_USER_ID = 1; + KNOWN_CONTEXT_INSTALLATION_ID = 2; + KNOWN_CONTEXT_VERSION = 3; + } +} diff --git a/protocol/proto/resolve_configuration_error.proto b/protocol/proto/resolve_configuration_error.proto new file mode 100644 index 00000000..22f2e1fb --- /dev/null +++ b/protocol/proto/resolve_configuration_error.proto @@ -0,0 +1,14 @@ +// 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/resource_type.proto b/protocol/proto/resource_type.proto new file mode 100644 index 00000000..ccea6920 --- /dev/null +++ b/protocol/proto/resource_type.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.offline.proto; + +option optimize_for = CODE_SIZE; + +enum ResourceType { + OTHER = 0; + AUDIO = 1; + DRM = 2; + IMAGE = 3; + VIDEO = 4; +} diff --git a/protocol/proto/response_status.proto b/protocol/proto/response_status.proto new file mode 100644 index 00000000..a9ecadd7 --- /dev/null +++ b/protocol/proto/response_status.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.playlist_esperanto.proto; + +option objc_class_prefix = "ESP"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.playlist.esperanto.proto"; + +message ResponseStatus { + int32 status_code = 1; + string reason = 2; +} diff --git a/protocol/proto/restrictions.proto b/protocol/proto/restrictions.proto new file mode 100644 index 00000000..0661858c --- /dev/null +++ b/protocol/proto/restrictions.proto @@ -0,0 +1,31 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message Restrictions { + repeated string disallow_pausing_reasons = 1; + repeated string disallow_resuming_reasons = 2; + repeated string disallow_seeking_reasons = 3; + repeated string disallow_peeking_prev_reasons = 4; + repeated string disallow_peeking_next_reasons = 5; + repeated string disallow_skipping_prev_reasons = 6; + repeated string disallow_skipping_next_reasons = 7; + repeated string disallow_toggling_repeat_context_reasons = 8; + repeated string disallow_toggling_repeat_track_reasons = 9; + repeated string disallow_toggling_shuffle_reasons = 10; + repeated string disallow_set_queue_reasons = 11; + repeated string disallow_interrupting_playback_reasons = 12; + repeated string disallow_transferring_playback_reasons = 13; + repeated string disallow_remote_control_reasons = 14; + repeated string disallow_inserting_into_next_tracks_reasons = 15; + repeated string disallow_inserting_into_context_tracks_reasons = 16; + repeated string disallow_reordering_in_next_tracks_reasons = 17; + repeated string disallow_reordering_in_context_tracks_reasons = 18; + repeated string disallow_removing_from_next_tracks_reasons = 19; + repeated string disallow_removing_from_context_tracks_reasons = 20; + repeated string disallow_updating_context_reasons = 21; +} diff --git a/protocol/proto/resume_points_node.proto b/protocol/proto/resume_points_node.proto new file mode 100644 index 00000000..9f7eed8e --- /dev/null +++ b/protocol/proto/resume_points_node.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify_shows.proto; + +option optimize_for = CODE_SIZE; + +message ResumePointsNode { + optional int64 resume_point = 1; +} diff --git a/protocol/proto/rootlist_request.proto b/protocol/proto/rootlist_request.proto new file mode 100644 index 00000000..80af73f0 --- /dev/null +++ b/protocol/proto/rootlist_request.proto @@ -0,0 +1,43 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.playlist.cosmos.rootlist_request.proto; + +import "playlist_folder_state.proto"; +import "playlist_playlist_state.proto"; +import "protobuf_delta.proto"; + +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.playlist.proto"; + +message Playlist { + optional string row_id = 1; + optional cosmos.proto.PlaylistMetadata playlist_metadata = 2; + optional cosmos.proto.PlaylistOfflineState playlist_offline_state = 3; + optional uint32 add_time = 4; + optional bool is_on_demand_in_free = 5; + optional string group_label = 6; +} + +message Item { + optional string header_field = 1; + optional Folder folder = 2; + optional Playlist playlist = 3; + optional protobuf_deltas.proto.Delta delta = 4; +} + +message Folder { + repeated Item item = 1; + optional cosmos.proto.FolderMetadata folder_metadata = 2; + optional string row_id = 3; + optional uint32 add_time = 4; + optional string group_label = 5; +} + +message Response { + optional Folder root = 1; + optional int32 unfiltered_length = 2; + optional int32 unranged_length = 3; + optional bool is_loading_contents = 4; +} diff --git a/protocol/proto/search.proto b/protocol/proto/search.proto deleted file mode 100644 index 38b717f7..00000000 --- a/protocol/proto/search.proto +++ /dev/null @@ -1,44 +0,0 @@ -syntax = "proto2"; - -message SearchRequest { - optional string query = 0x1; - optional Type type = 0x2; - enum Type { - TRACK = 0x0; - ALBUM = 0x1; - ARTIST = 0x2; - PLAYLIST = 0x3; - USER = 0x4; - } - optional int32 limit = 0x3; - optional int32 offset = 0x4; - optional bool did_you_mean = 0x5; - optional string spotify_uri = 0x2; - repeated bytes file_id = 0x3; - optional string url = 0x4; - optional string slask_id = 0x5; -} - -message Playlist { - optional string uri = 0x1; - optional string name = 0x2; - repeated Image image = 0x3; -} - -message User { - optional string username = 0x1; - optional string full_name = 0x2; - repeated Image image = 0x3; - optional sint32 followers = 0x4; -} - -message SearchReply { - optional sint32 hits = 0x1; - repeated Track track = 0x2; - repeated Album album = 0x3; - repeated Artist artist = 0x4; - repeated Playlist playlist = 0x5; - optional string did_you_mean = 0x6; - repeated User user = 0x7; -} - diff --git a/protocol/proto/seek_to_position.proto b/protocol/proto/seek_to_position.proto new file mode 100644 index 00000000..6f426842 --- /dev/null +++ b/protocol/proto/seek_to_position.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message SeekToPosition { + optional uint64 value = 1; + optional uint32 revision = 2; +} diff --git a/protocol/proto/sequence_number_entity.proto b/protocol/proto/sequence_number_entity.proto new file mode 100644 index 00000000..cd97392c --- /dev/null +++ b/protocol/proto/sequence_number_entity.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.event_sender.proto; + +option optimize_for = CODE_SIZE; + +message SequenceNumberEntity { + int32 file_format_version = 1; + string event_name = 2; + bytes sequence_id = 3; + int64 sequence_number_next = 4; +} diff --git a/protocol/proto/session.proto b/protocol/proto/session.proto new file mode 100644 index 00000000..7c4589f3 --- /dev/null +++ b/protocol/proto/session.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto.transfer; + +import "context.proto"; +import "context_player_options.proto"; +import "play_origin.proto"; +import "suppressions.proto"; +import "instrumentation_params.proto"; + +option optimize_for = CODE_SIZE; + +message Session { + optional PlayOrigin play_origin = 1; + optional Context context = 2; + optional string current_uid = 3; + optional ContextPlayerOptionOverrides option_overrides = 4; + optional Suppressions suppressions = 5; + optional InstrumentationParams instrumentation_params = 6; +} diff --git a/protocol/proto/show_access.proto b/protocol/proto/show_access.proto new file mode 100644 index 00000000..3516cdfd --- /dev/null +++ b/protocol/proto/show_access.proto @@ -0,0 +1,33 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.podcast_paywalls; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "ShowAccessProto"; +option java_package = "com.spotify.podcast.access.proto"; + +message ShowAccess { + oneof explanation { + NoExplanation none = 1; + LegacyExplanation legacy = 2; + BasicExplanation basic = 3; + } +} + +message BasicExplanation { + string title = 1; + string body = 2; + string cta = 3; +} + +message LegacyExplanation { + +} + +message NoExplanation { + +} diff --git a/protocol/proto/show_episode_state.proto b/protocol/proto/show_episode_state.proto new file mode 100644 index 00000000..001fafee --- /dev/null +++ b/protocol/proto/show_episode_state.proto @@ -0,0 +1,25 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.show_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message EpisodeCollectionState { + optional bool is_following_show = 1; + optional bool is_new = 2; + optional bool is_in_listen_later = 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 new file mode 100644 index 00000000..0f40a1bd --- /dev/null +++ b/protocol/proto/show_request.proto @@ -0,0 +1,66 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.show_cosmos.proto; + +import "metadata/episode_metadata.proto"; +import "metadata/show_metadata.proto"; +import "show_episode_state.proto"; +import "show_show_state.proto"; +import "podcast_virality.proto"; +import "transcripts.proto"; +import "podcastextensions.proto"; +import "clips_cover.proto"; +import "show_access.proto"; + +option optimize_for = CODE_SIZE; + +message Item { + optional string header_field = 1; + 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 corex.transcripts.metadata.EpisodeTranscript episode_transcripts = 7; + optional podcastvirality.v1.PodcastVirality episode_virality = 8; + + reserved 6; +} + +message Header { + optional cosmos_util.proto.ShowMetadata show_metadata = 1; + optional ShowCollectionState show_collection_state = 2; + optional ShowPlayState show_play_state = 3; +} + +message Response { + repeated Item item = 1; + optional Header header = 2; + optional uint32 unfiltered_length = 4; + optional uint32 length = 5; + optional bool loading_contents = 6; + optional uint32 unranged_length = 7; + optional AuxiliarySections auxiliary_sections = 8; + optional podcast_paywalls.ShowAccess access_info = 9; + + reserved 3, "online_data"; +} + +message AuxiliarySections { + optional ContinueListeningSection continue_listening = 1; + optional podcast.extensions.PodcastTopics topics_section = 2; + optional TrailerSection trailer_section = 3; + optional podcast.extensions.PodcastHtmlDescription html_description_section = 5; + optional clips.ClipsCover clips_section = 6; + + reserved 4; +} + +message ContinueListeningSection { + optional Item item = 1; +} + +message TrailerSection { + optional Item item = 1; +} diff --git a/protocol/proto/show_show_state.proto b/protocol/proto/show_show_state.proto new file mode 100644 index 00000000..ab0d1fe3 --- /dev/null +++ b/protocol/proto/show_show_state.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.show_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message ShowCollectionState { + optional bool is_in_collection = 1; +} + +message ShowPlayState { + optional string latest_played_episode_link = 1; +} diff --git a/protocol/proto/skip_to_track.proto b/protocol/proto/skip_to_track.proto new file mode 100644 index 00000000..67b5f717 --- /dev/null +++ b/protocol/proto/skip_to_track.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message SkipToTrack { + optional string page_url = 1; + optional uint64 page_index = 2; + optional string track_uid = 3; + optional string track_uri = 4; + optional uint64 track_index = 5; +} diff --git a/protocol/proto/social.proto b/protocol/proto/social.proto deleted file mode 100644 index 58d39a18..00000000 --- a/protocol/proto/social.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto2"; - -message DecorationData { - optional string username = 0x1; - optional string full_name = 0x2; - optional string image_url = 0x3; - optional string large_image_url = 0x5; - optional string first_name = 0x6; - optional string last_name = 0x7; - optional string facebook_uid = 0x8; -} - diff --git a/protocol/proto/social_connect_v2.proto b/protocol/proto/social_connect_v2.proto new file mode 100644 index 00000000..265fbee6 --- /dev/null +++ b/protocol/proto/social_connect_v2.proto @@ -0,0 +1,55 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package socialconnect; + +option optimize_for = CODE_SIZE; + +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 = 9; +} + +message SessionMember { + int64 timestamp = 1; + string id = 2; + string username = 3; + string display_name = 4; + string image_url = 5; + string large_image_url = 6; +} + +message SessionUpdate { + Session session = 1; + SessionUpdateReason reason = 2; + repeated SessionMember updated_session_members = 3; +} + +message DevicesExposure { + int64 timestamp = 1; + map devices_exposure = 2; +} + +enum SessionUpdateReason { + UNKNOWN_UPDATE_TYPE = 0; + NEW_SESSION = 1; + USER_JOINED = 2; + USER_LEFT = 3; + SESSION_DELETED = 4; + YOU_LEFT = 5; + YOU_WERE_KICKED = 6; + YOU_JOINED = 7; +} + +enum DeviceExposureStatus { + NOT_EXPOSABLE = 0; + NOT_EXPOSED = 1; + EXPOSED = 2; +} diff --git a/protocol/proto/socialgraph.proto b/protocol/proto/socialgraph.proto deleted file mode 100644 index 3adc1306..00000000 --- a/protocol/proto/socialgraph.proto +++ /dev/null @@ -1,49 +0,0 @@ -syntax = "proto2"; - -message CountReply { - repeated int32 counts = 0x1; -} - -message UserListRequest { - optional string last_result = 0x1; - optional int32 count = 0x2; - optional bool include_length = 0x3; -} - -message UserListReply { - repeated User users = 0x1; - optional int32 length = 0x2; -} - -message User { - optional string username = 0x1; - optional int32 subscriber_count = 0x2; - optional int32 subscription_count = 0x3; -} - -message ArtistListReply { - repeated Artist artists = 0x1; -} - -message Artist { - optional string artistid = 0x1; - optional int32 subscriber_count = 0x2; -} - -message StringListRequest { - repeated string args = 0x1; -} - -message StringListReply { - repeated string reply = 0x1; -} - -message TopPlaylistsRequest { - optional string username = 0x1; - optional int32 count = 0x2; -} - -message TopPlaylistsReply { - repeated string uris = 0x1; -} - diff --git a/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto b/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto new file mode 100644 index 00000000..92d50f42 --- /dev/null +++ b/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto @@ -0,0 +1,123 @@ +// Extracted from: Spotify 1.1.33.569 (Windows) + +syntax = "proto3"; + +package spotify.clienttoken.http.v0; + +import "connectivity.proto"; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "spotify.clienttoken.http.v0"; + +message ClientTokenRequest { + ClientTokenRequestType request_type = 1; + + oneof request { + ClientDataRequest client_data = 2; + ChallengeAnswersRequest challenge_answers = 3; + } +} + +message ClientDataRequest { + string client_version = 1; + string client_id = 2; + + oneof data { + data.v0.ConnectivitySdkData connectivity_sdk_data = 3; + } +} + +message ChallengeAnswersRequest { + string state = 1; + repeated ChallengeAnswer answers = 2; +} + +message ClientTokenResponse { + ClientTokenResponseType response_type = 1; + + oneof response { + GrantedTokenResponse granted_token = 2; + ChallengesResponse challenges = 3; + } +} + +message GrantedTokenResponse { + string token = 1; + int32 expires_after_seconds = 2; + int32 refresh_after_seconds = 3; +} + +message ChallengesResponse { + string state = 1; + repeated Challenge challenges = 2; +} + +message ClientSecretParameters { + string salt = 1; +} + +message EvaluateJSParameters { + string code = 1; + repeated string libraries = 2; +} + +message HashCashParameters { + int32 length = 1; + string prefix = 2; +} + +message Challenge { + ChallengeType type = 1; + + oneof parameters { + ClientSecretParameters client_secret_parameters = 2; + EvaluateJSParameters evaluate_js_parameters = 3; + HashCashParameters evaluate_hashcash_parameters = 4; + } +} + +message ClientSecretHMACAnswer { + string hmac = 1; +} + +message EvaluateJSAnswer { + string result = 1; +} + +message HashCashAnswer { + string suffix = 1; +} + +message ChallengeAnswer { + ChallengeType ChallengeType = 1; + + oneof answer { + ClientSecretHMACAnswer client_secret = 2; + EvaluateJSAnswer evaluate_js = 3; + HashCashAnswer hash_cash = 4; + } +} + +message ClientTokenBadRequest { + string message = 1; +} + +enum ClientTokenRequestType { + REQUEST_UNKNOWN = 0; + REQUEST_CLIENT_DATA_REQUEST = 1; + REQUEST_CHALLENGE_ANSWERS_REQUEST = 2; +} + +enum ClientTokenResponseType { + RESPONSE_UNKNOWN = 0; + RESPONSE_GRANTED_TOKEN_RESPONSE = 1; + RESPONSE_CHALLENGES_RESPONSE = 2; +} + +enum ChallengeType { + CHALLENGE_UNKNOWN = 0; + CHALLENGE_CLIENT_SECRET_HMAC = 1; + CHALLENGE_EVALUATE_JS = 2; + CHALLENGE_HASH_CASH = 3; +} diff --git a/protocol/proto/spotify/login5/v3/challenges/code.proto b/protocol/proto/spotify/login5/v3/challenges/code.proto new file mode 100644 index 00000000..980d3de3 --- /dev/null +++ b/protocol/proto/spotify/login5/v3/challenges/code.proto @@ -0,0 +1,26 @@ +// Extracted from: Spotify 1.1.33.569 (Windows) + +syntax = "proto3"; + +package spotify.login5.v3.challenges; + +option objc_class_prefix = "SPTLogin5"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.login5.v3.challenges.proto"; + +message CodeChallenge { + Method method = 1; + enum Method { + UNKNOWN = 0; + SMS = 1; + } + + int32 code_length = 2; + int32 expires_in = 3; + string canonical_phone_number = 4; +} + +message CodeSolution { + string code = 1; +} diff --git a/protocol/proto/spotify/login5/v3/challenges/hashcash.proto b/protocol/proto/spotify/login5/v3/challenges/hashcash.proto new file mode 100644 index 00000000..3e83981c --- /dev/null +++ b/protocol/proto/spotify/login5/v3/challenges/hashcash.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.33.569 (Windows) + +syntax = "proto3"; + +package spotify.login5.v3.challenges; + +import "google/protobuf/duration.proto"; + +option objc_class_prefix = "SPTLogin5"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.login5.v3.challenges.proto"; + +message HashcashChallenge { + bytes prefix = 1; + int32 length = 2; +} + +message HashcashSolution { + bytes suffix = 1; + google.protobuf.Duration duration = 2; +} diff --git a/protocol/proto/spotify/login5/v3/client_info.proto b/protocol/proto/spotify/login5/v3/client_info.proto new file mode 100644 index 00000000..575891e1 --- /dev/null +++ b/protocol/proto/spotify/login5/v3/client_info.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.33.569 (Windows) + +syntax = "proto3"; + +package spotify.login5.v3; + +option objc_class_prefix = "SPTLogin5"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.login5.v3.proto"; + +message ClientInfo { + string client_id = 1; + string device_id = 2; +} diff --git a/protocol/proto/spotify/login5/v3/credentials/credentials.proto b/protocol/proto/spotify/login5/v3/credentials/credentials.proto new file mode 100644 index 00000000..defab249 --- /dev/null +++ b/protocol/proto/spotify/login5/v3/credentials/credentials.proto @@ -0,0 +1,48 @@ +// Extracted from: Spotify 1.1.33.569 (Windows) + +syntax = "proto3"; + +package spotify.login5.v3.credentials; + +option objc_class_prefix = "SPTLogin5"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.login5.v3.credentials.proto"; + +message StoredCredential { + string username = 1; + bytes data = 2; +} + +message Password { + string id = 1; + string password = 2; + bytes padding = 3; +} + +message FacebookAccessToken { + string fb_uid = 1; + string access_token = 2; +} + +message OneTimeToken { + string token = 1; +} + +message ParentChildCredential { + string child_id = 1; + StoredCredential parent_stored_credential = 2; +} + +message AppleSignInCredential { + string auth_code = 1; + string redirect_uri = 2; + string bundle_id = 3; +} + +message SamsungSignInCredential { + string auth_code = 1; + string redirect_uri = 2; + string id_token = 3; + string token_endpoint_url = 4; +} diff --git a/protocol/proto/spotify/login5/v3/identifiers/identifiers.proto b/protocol/proto/spotify/login5/v3/identifiers/identifiers.proto new file mode 100644 index 00000000..b82e9942 --- /dev/null +++ b/protocol/proto/spotify/login5/v3/identifiers/identifiers.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.33.569 (Windows) + +syntax = "proto3"; + +package spotify.login5.v3.identifiers; + +option objc_class_prefix = "SPTLogin5"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.login5.v3.identifiers.proto"; + +message PhoneNumber { + string number = 1; + string iso_country_code = 2; + string country_calling_code = 3; +} diff --git a/protocol/proto/spotify/login5/v3/login5.proto b/protocol/proto/spotify/login5/v3/login5.proto new file mode 100644 index 00000000..f10ada21 --- /dev/null +++ b/protocol/proto/spotify/login5/v3/login5.proto @@ -0,0 +1,93 @@ +// Extracted from: Spotify 1.1.33.569 (Windows) + +syntax = "proto3"; + +package spotify.login5.v3; + +import "spotify/login5/v3/client_info.proto"; +import "spotify/login5/v3/user_info.proto"; +import "spotify/login5/v3/challenges/code.proto"; +import "spotify/login5/v3/challenges/hashcash.proto"; +import "spotify/login5/v3/credentials/credentials.proto"; +import "spotify/login5/v3/identifiers/identifiers.proto"; + +option objc_class_prefix = "SPTLogin5"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.login5.v3.proto"; + +message Challenges { + repeated Challenge challenges = 1; +} + +message Challenge { + oneof challenge { + challenges.HashcashChallenge hashcash = 1; + challenges.CodeChallenge code = 2; + } +} + +message ChallengeSolutions { + repeated ChallengeSolution solutions = 1; +} + +message ChallengeSolution { + oneof solution { + challenges.HashcashSolution hashcash = 1; + challenges.CodeSolution code = 2; + } +} + +message LoginRequest { + ClientInfo client_info = 1; + bytes login_context = 2; + ChallengeSolutions challenge_solutions = 3; + + oneof login_method { + credentials.StoredCredential stored_credential = 100; + credentials.Password password = 101; + credentials.FacebookAccessToken facebook_access_token = 102; + identifiers.PhoneNumber phone_number = 103; + credentials.OneTimeToken one_time_token = 104; + credentials.ParentChildCredential parent_child_credential = 105; + credentials.AppleSignInCredential apple_sign_in_credential = 106; + credentials.SamsungSignInCredential samsung_sign_in_credential = 107; + } +} + +message LoginOk { + string username = 1; + string access_token = 2; + bytes stored_credential = 3; + int32 access_token_expires_in = 4; +} + +message LoginResponse { + repeated Warnings warnings = 4; + enum Warnings { + UNKNOWN_WARNING = 0; + DEPRECATED_PROTOCOL_VERSION = 1; + } + + bytes login_context = 5; + string identifier_token = 6; + UserInfo user_info = 7; + + oneof response { + LoginOk ok = 1; + LoginError error = 2; + Challenges challenges = 3; + } +} + +enum LoginError { + UNKNOWN_ERROR = 0; + INVALID_CREDENTIALS = 1; + BAD_REQUEST = 2; + UNSUPPORTED_LOGIN_PROTOCOL = 3; + TIMEOUT = 4; + UNKNOWN_IDENTIFIER = 5; + TOO_MANY_ATTEMPTS = 6; + INVALID_PHONENUMBER = 7; + TRY_AGAIN_LATER = 8; +} diff --git a/protocol/proto/spotify/login5/v3/user_info.proto b/protocol/proto/spotify/login5/v3/user_info.proto new file mode 100644 index 00000000..a7e040cc --- /dev/null +++ b/protocol/proto/spotify/login5/v3/user_info.proto @@ -0,0 +1,29 @@ +// Extracted from: Spotify 1.1.33.569 (Windows) + +syntax = "proto3"; + +package spotify.login5.v3; + +option objc_class_prefix = "SPTLogin5"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.login5.v3.proto"; + +message UserInfo { + string name = 1; + string email = 2; + bool email_verified = 3; + string birthdate = 4; + + Gender gender = 5; + enum Gender { + UNKNOWN = 0; + MALE = 1; + FEMALE = 2; + NEUTRAL = 3; + } + + string phone_number = 6; + bool phone_number_verified = 7; + bool email_already_registered = 8; +} diff --git a/protocol/proto/status_code.proto b/protocol/proto/status_code.proto new file mode 100644 index 00000000..8e813d25 --- /dev/null +++ b/protocol/proto/status_code.proto @@ -0,0 +1,12 @@ +// 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 StatusCode { + SUCCESS = 0; +} diff --git a/protocol/proto/status_response.proto b/protocol/proto/status_response.proto new file mode 100644 index 00000000..78d15c9a --- /dev/null +++ b/protocol/proto/status_response.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.stream_reporting_esperanto.proto; + +import "status_code.proto"; + +option objc_class_prefix = "ESP"; +option java_package = "com.spotify.stream_reporting_esperanto.proto"; + +message StatusResponse { + StatusCode status_code = 1; + string reason = 2; +} diff --git a/protocol/proto/storage-resolve.proto b/protocol/proto/storage-resolve.proto new file mode 100644 index 00000000..1cb3b673 --- /dev/null +++ b/protocol/proto/storage-resolve.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.download.proto; + +option optimize_for = CODE_SIZE; + +message StorageResolveResponse { + Result result = 1; + enum Result { + CDN = 0; + STORAGE = 1; + RESTRICTED = 3; + } + + repeated string cdnurl = 2; + bytes fileid = 4; +} diff --git a/protocol/proto/storage_cosmos.proto b/protocol/proto/storage_cosmos.proto new file mode 100644 index 00000000..97169850 --- /dev/null +++ b/protocol/proto/storage_cosmos.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.storage_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message GetFileCacheRangesResponse { + bool byte_size_known = 1; + uint64 byte_size = 2; + + repeated Range ranges = 3; + message Range { + uint64 from_byte = 1; + uint64 to_byte = 2; + } +} diff --git a/protocol/proto/storylines.proto b/protocol/proto/storylines.proto new file mode 100644 index 00000000..c9361966 --- /dev/null +++ b/protocol/proto/storylines.proto @@ -0,0 +1,29 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.storylines.v1; + +option java_multiple_files = true; +option java_outer_classname = "StorylinesProto"; +option java_package = "com.spotify.storylines.v1.extended_metadata"; + +message Artist { + string uri = 1; + string name = 2; + string avatar_cdn_url = 3; +} + +message Card { + string id = 1; + string image_cdn_url = 2; + int32 image_width = 3; + int32 image_height = 4; +} + +message Storyline { + string id = 1; + string entity_uri = 2; + Artist artist = 3; + repeated Card cards = 4; +} diff --git a/protocol/proto/stream_end_request.proto b/protocol/proto/stream_end_request.proto new file mode 100644 index 00000000..5ef8be7f --- /dev/null +++ b/protocol/proto/stream_end_request.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.stream_reporting_esperanto.proto; + +import "stream_handle.proto"; +import "play_reason.proto"; +import "play_source.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; + PlayReason reason_end = 3; +} diff --git a/protocol/proto/stream_handle.proto b/protocol/proto/stream_handle.proto new file mode 100644 index 00000000..b66ed4ce --- /dev/null +++ b/protocol/proto/stream_handle.proto @@ -0,0 +1,12 @@ +// 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"; + +message StreamHandle { + string playback_id = 1; +} diff --git a/protocol/proto/stream_prepare_request.proto b/protocol/proto/stream_prepare_request.proto new file mode 100644 index 00000000..ce22e8eb --- /dev/null +++ b/protocol/proto/stream_prepare_request.proto @@ -0,0 +1,39 @@ +// 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_prepare_response.proto b/protocol/proto/stream_prepare_response.proto new file mode 100644 index 00000000..2f5a2c4e --- /dev/null +++ b/protocol/proto/stream_prepare_response.proto @@ -0,0 +1,18 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.stream_reporting_esperanto.proto; + +import "status_response.proto"; +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; + } +} diff --git a/protocol/proto/stream_progress_request.proto b/protocol/proto/stream_progress_request.proto new file mode 100644 index 00000000..63fe9d80 --- /dev/null +++ b/protocol/proto/stream_progress_request.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.stream_reporting_esperanto.proto; + +import "stream_handle.proto"; + +option objc_class_prefix = "ESP"; +option java_package = "com.spotify.stream_reporting_esperanto.proto"; + +message StreamProgressRequest { + StreamHandle stream_handle = 1; + uint64 current_position = 2; + bool is_paused = 3; + bool is_playing_video = 4; + bool is_overlapping = 5; + bool is_background = 6; + bool is_fullscreen = 7; + bool is_external = 8; + double playback_speed = 9; +} diff --git a/protocol/proto/stream_seek_request.proto b/protocol/proto/stream_seek_request.proto new file mode 100644 index 00000000..3736abf9 --- /dev/null +++ b/protocol/proto/stream_seek_request.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.stream_reporting_esperanto.proto; + +import "stream_handle.proto"; + +option objc_class_prefix = "ESP"; +option java_package = "com.spotify.stream_reporting_esperanto.proto"; + +message StreamSeekRequest { + StreamHandle stream_handle = 1; +} diff --git a/protocol/proto/stream_start_request.proto b/protocol/proto/stream_start_request.proto new file mode 100644 index 00000000..3c4bfbb6 --- /dev/null +++ b/protocol/proto/stream_start_request.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.stream_reporting_esperanto.proto; + +import "format.proto"; +import "media_type.proto"; +import "stream_handle.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; +} diff --git a/protocol/proto/streaming_rule.proto b/protocol/proto/streaming_rule.proto new file mode 100644 index 00000000..d72d7ca5 --- /dev/null +++ b/protocol/proto/streaming_rule.proto @@ -0,0 +1,17 @@ +// 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 StreamingRule { + RULE_UNSET = 0; + RULE_NONE = 1; + RULE_DMCA_RADIO = 2; + RULE_PREVIEW = 3; + RULE_WIFI = 4; + RULE_SHUFFLE_MODE = 5; +} diff --git a/protocol/proto/suggest.proto b/protocol/proto/suggest.proto deleted file mode 100644 index ef45f1e2..00000000 --- a/protocol/proto/suggest.proto +++ /dev/null @@ -1,43 +0,0 @@ -syntax = "proto2"; - -message Track { - optional bytes gid = 0x1; - optional string name = 0x2; - optional bytes image = 0x3; - repeated string artist_name = 0x4; - repeated bytes artist_gid = 0x5; - optional uint32 rank = 0x6; -} - -message Artist { - optional bytes gid = 0x1; - optional string name = 0x2; - optional bytes image = 0x3; - optional uint32 rank = 0x6; -} - -message Album { - optional bytes gid = 0x1; - optional string name = 0x2; - optional bytes image = 0x3; - repeated string artist_name = 0x4; - repeated bytes artist_gid = 0x5; - optional uint32 rank = 0x6; -} - -message Playlist { - optional string uri = 0x1; - optional string name = 0x2; - optional string image_uri = 0x3; - optional string owner_name = 0x4; - optional string owner_uri = 0x5; - optional uint32 rank = 0x6; -} - -message Suggestions { - repeated Track track = 0x1; - repeated Album album = 0x2; - repeated Artist artist = 0x3; - repeated Playlist playlist = 0x4; -} - diff --git a/protocol/proto/suppressions.proto b/protocol/proto/suppressions.proto new file mode 100644 index 00000000..4ddfaefb --- /dev/null +++ b/protocol/proto/suppressions.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message Suppressions { + repeated string providers = 1; +} diff --git a/protocol/proto/sync/album_sync_state.proto b/protocol/proto/sync/album_sync_state.proto new file mode 100644 index 00000000..7ea90276 --- /dev/null +++ b/protocol/proto/sync/album_sync_state.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message AlbumSyncState { + optional string offline = 1; + optional string inferred_offline = 2; + optional uint32 sync_progress = 3; +} diff --git a/protocol/proto/sync/artist_sync_state.proto b/protocol/proto/sync/artist_sync_state.proto new file mode 100644 index 00000000..03ba32f3 --- /dev/null +++ b/protocol/proto/sync/artist_sync_state.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message ArtistSyncState { + optional string offline = 1; + optional string inferred_offline = 2; + optional uint32 sync_progress = 3; +} diff --git a/protocol/proto/sync/episode_sync_state.proto b/protocol/proto/sync/episode_sync_state.proto new file mode 100644 index 00000000..7dce8424 --- /dev/null +++ b/protocol/proto/sync/episode_sync_state.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message EpisodeSyncState { + optional string offline_state = 1; + optional uint32 sync_progress = 2; +} diff --git a/protocol/proto/sync/track_sync_state.proto b/protocol/proto/sync/track_sync_state.proto new file mode 100644 index 00000000..8873fad5 --- /dev/null +++ b/protocol/proto/sync/track_sync_state.proto @@ -0,0 +1,14 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.cosmos_util.proto; + +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_package = "com.spotify.cosmos.util.proto"; + +message TrackSyncState { + optional string offline = 1; + optional uint32 sync_progress = 2; +} diff --git a/protocol/proto/sync_request.proto b/protocol/proto/sync_request.proto new file mode 100644 index 00000000..090f8dce --- /dev/null +++ b/protocol/proto/sync_request.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +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 SyncRequest { + repeated string playlist_uris = 1; +} diff --git a/protocol/proto/techu_core_exercise_cosmos.proto b/protocol/proto/techu_core_exercise_cosmos.proto new file mode 100644 index 00000000..155a303f --- /dev/null +++ b/protocol/proto/techu_core_exercise_cosmos.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.techu_core_exercise_cosmos.proto; + +option optimize_for = CODE_SIZE; + +message TechUCoreExerciseRequest { + string a = 1; + string b = 2; +} + +message TechUCoreExerciseResponse { + string concatenated = 1; +} diff --git a/protocol/proto/test_request_failure.proto b/protocol/proto/test_request_failure.proto new file mode 100644 index 00000000..036e38e1 --- /dev/null +++ b/protocol/proto/test_request_failure.proto @@ -0,0 +1,14 @@ +// 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/toplist.proto b/protocol/proto/toplist.proto deleted file mode 100644 index 1a12159f..00000000 --- a/protocol/proto/toplist.proto +++ /dev/null @@ -1,6 +0,0 @@ -syntax = "proto2"; - -message Toplist { - repeated string items = 0x1; -} - diff --git a/protocol/proto/track_instance.proto b/protocol/proto/track_instance.proto new file mode 100644 index 00000000..952f28c8 --- /dev/null +++ b/protocol/proto/track_instance.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +import "context_index.proto"; +import "context_track.proto"; +import "seek_to_position.proto"; + +option optimize_for = CODE_SIZE; + +message TrackInstance { + optional ContextTrack track = 1; + optional uint64 id = 2; + optional SeekToPosition seek_to_position = 7; + optional bool initially_paused = 4; + optional ContextIndex index = 5; + optional string provider = 6; + + reserved 3; +} diff --git a/protocol/proto/track_instantiator.proto b/protocol/proto/track_instantiator.proto new file mode 100644 index 00000000..3b8b8baf --- /dev/null +++ b/protocol/proto/track_instantiator.proto @@ -0,0 +1,13 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message TrackInstantiator { + optional uint64 unique = 1; + optional uint64 count = 2; + optional string provider = 3; +} diff --git a/protocol/proto/track_offlining_cosmos_response.proto b/protocol/proto/track_offlining_cosmos_response.proto new file mode 100644 index 00000000..bb650607 --- /dev/null +++ b/protocol/proto/track_offlining_cosmos_response.proto @@ -0,0 +1,24 @@ +// 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/transcripts.proto b/protocol/proto/transcripts.proto new file mode 100644 index 00000000..05ac7fbb --- /dev/null +++ b/protocol/proto/transcripts.proto @@ -0,0 +1,23 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.corex.transcripts.metadata; + +option objc_class_prefix = "SPT"; +option java_multiple_files = true; +option optimize_for = CODE_SIZE; +option java_outer_classname = "TranscriptMetadataProto"; +option java_package = "com.spotify.corex.transcripts.metadata.proto"; + +message EpisodeTranscript { + string episode_uri = 1; + repeated Transcript transcripts = 2; +} + +message Transcript { + string uri = 1; + string language = 2; + bool curated = 3; + string cdn_url = 4; +} diff --git a/protocol/proto/transfer_node.proto b/protocol/proto/transfer_node.proto new file mode 100644 index 00000000..e5bbc03e --- /dev/null +++ b/protocol/proto/transfer_node.proto @@ -0,0 +1,15 @@ +// 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 TransferNode { + optional TrackInstance instance = 1; + optional TrackInstantiator instantiator = 2; +} diff --git a/protocol/proto/transfer_state.proto b/protocol/proto/transfer_state.proto new file mode 100644 index 00000000..200547c0 --- /dev/null +++ b/protocol/proto/transfer_state.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.player.proto.transfer; + +import "context_player_options.proto"; +import "playback.proto"; +import "session.proto"; +import "queue.proto"; + +option optimize_for = CODE_SIZE; + +message TransferState { + optional ContextPlayerOptions options = 1; + optional Playback playback = 2; + optional Session current_session = 3; + optional Queue queue = 4; +} diff --git a/protocol/proto/tts-resolve.proto b/protocol/proto/tts-resolve.proto new file mode 100644 index 00000000..89956843 --- /dev/null +++ b/protocol/proto/tts-resolve.proto @@ -0,0 +1,49 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.narration_injection.proto; + +option optimize_for = CODE_SIZE; + +service TtsResolveService { + rpc Resolve(ResolveRequest) returns (ResolveResponse); +} + +message ResolveRequest { + AudioFormat audio_format = 3; + enum AudioFormat { + UNSPECIFIED = 0; + WAV = 1; + PCM = 2; + OPUS = 3; + VORBIS = 4; + MP3 = 5; + } + + string language = 4; + + TtsVoice tts_voice = 5; + enum TtsVoice { + UNSET_TTS_VOICE = 0; + VOICE1 = 1; + VOICE2 = 2; + VOICE3 = 3; + } + + TtsProvider tts_provider = 6; + enum TtsProvider { + UNSET_TTS_PROVIDER = 0; + CLOUD_TTS = 1; + READSPEAKER = 2; + } + + oneof prompt { + string text = 1; + string ssml = 2; + } +} + +message ResolveResponse { + string url = 1; +} diff --git a/protocol/proto/ucs.proto b/protocol/proto/ucs.proto new file mode 100644 index 00000000..c5048f8c --- /dev/null +++ b/protocol/proto/ucs.proto @@ -0,0 +1,56 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.remote_config.ucs.proto; + +import "resolve.proto"; +import "useraccount.proto"; + +option optimize_for = CODE_SIZE; + +message UcsRequest { + CallerInfo caller_info = 1; + message CallerInfo { + string request_origin_id = 1; + string request_orgin_version = 2; + string reason = 3; + } + + ResolveRequest resolve_request = 2; + + AccountAttributesRequest account_attributes_request = 3; + message AccountAttributesRequest { + + } +} + +message UcsResponseWrapper { + oneof result { + UcsResponse success = 1; + Error error = 2; + } + + message UcsResponse { + int64 fetch_time_millis = 5; + + oneof resolve_result { + ResolveResponse resolve_success = 1; + Error resolve_error = 2; + } + + oneof account_attributes_result { + AccountAttributesResponse account_attributes_success = 3; + Error account_attributes_error = 4; + } + } + + message AccountAttributesResponse { + map account_attributes = 1; + } + + message Error { + int32 error_code = 1; + string error_message = 2; + } +} diff --git a/protocol/proto/unfinished_episodes_request.proto b/protocol/proto/unfinished_episodes_request.proto new file mode 100644 index 00000000..1e152bd6 --- /dev/null +++ b/protocol/proto/unfinished_episodes_request.proto @@ -0,0 +1,24 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto2"; + +package spotify.show_cosmos.unfinished_episodes_request.proto; + +import "metadata/episode_metadata.proto"; +import "show_episode_state.proto"; + +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 string link = 5; +} + +message Response { + repeated Episode episode = 2; + + reserved 1; +} diff --git a/protocol/proto/useraccount.proto b/protocol/proto/useraccount.proto new file mode 100644 index 00000000..ca8fea90 --- /dev/null +++ b/protocol/proto/useraccount.proto @@ -0,0 +1,15 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.remote_config.ucs.proto; + +option optimize_for = CODE_SIZE; + +message AccountAttribute { + oneof value { + bool bool_value = 2; + int64 long_value = 3; + string string_value = 4; + } +} diff --git a/protocol/proto/your_library_contains_request.proto b/protocol/proto/your_library_contains_request.proto new file mode 100644 index 00000000..33672bad --- /dev/null +++ b/protocol/proto/your_library_contains_request.proto @@ -0,0 +1,11 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.your_library.proto; + +option optimize_for = CODE_SIZE; + +message YourLibraryContainsRequest { + repeated string requested_uri = 3; +} diff --git a/protocol/proto/your_library_contains_response.proto b/protocol/proto/your_library_contains_response.proto new file mode 100644 index 00000000..641d71a5 --- /dev/null +++ b/protocol/proto/your_library_contains_response.proto @@ -0,0 +1,22 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.your_library.proto; + +option optimize_for = CODE_SIZE; + +message YourLibraryContainsResponseHeader { + bool is_loading = 2; +} + +message YourLibraryContainsResponseEntity { + string uri = 1; + bool is_in_library = 2; +} + +message YourLibraryContainsResponse { + YourLibraryContainsResponseHeader header = 1; + repeated YourLibraryContainsResponseEntity entity = 2; + string error = 99; +} diff --git a/protocol/proto/your_library_decorate_request.proto b/protocol/proto/your_library_decorate_request.proto new file mode 100644 index 00000000..e3fccc29 --- /dev/null +++ b/protocol/proto/your_library_decorate_request.proto @@ -0,0 +1,17 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.your_library.proto; + +import "your_library_request.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; +} diff --git a/protocol/proto/your_library_decorate_response.proto b/protocol/proto/your_library_decorate_response.proto new file mode 100644 index 00000000..dab14203 --- /dev/null +++ b/protocol/proto/your_library_decorate_response.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.your_library.proto; + +import "your_library_response.proto"; + +option optimize_for = CODE_SIZE; + +message YourLibraryDecorateResponseHeader { + bool is_loading = 2; +} + +message YourLibraryDecorateResponse { + YourLibraryDecorateResponseHeader header = 1; + repeated YourLibraryResponseEntity entity = 2; + string error = 99; +} diff --git a/protocol/proto/your_library_entity.proto b/protocol/proto/your_library_entity.proto new file mode 100644 index 00000000..acb5afe7 --- /dev/null +++ b/protocol/proto/your_library_entity.proto @@ -0,0 +1,21 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.your_library.proto; + +import "your_library_index.proto"; +import "collection_index.proto"; + +option optimize_for = CODE_SIZE; + +message YourLibraryEntity { + bool pinned = 1; + + oneof entity { + collection.proto.CollectionAlbumEntry album = 2; + YourLibraryArtistEntity artist = 3; + YourLibraryRootlistEntity rootlist_entity = 4; + YourLibraryShowEntity show = 5; + } +} diff --git a/protocol/proto/your_library_index.proto b/protocol/proto/your_library_index.proto new file mode 100644 index 00000000..2d452dd5 --- /dev/null +++ b/protocol/proto/your_library_index.proto @@ -0,0 +1,60 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +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; +} + +message YourLibraryRootlistFolder { + int32 number_of_playlists = 1; + int32 number_of_folders = 2; + int32 rootlist_index = 3; +} + +message YourLibraryRootlistCollection { + Kind kind = 1; + enum Kind { + LIKED_SONGS = 0; + YOUR_EPISODES = 1; + NEW_EPISODES = 2; + LOCAL_FILES = 3; + } +} + +message YourLibraryRootlistEntity { + string uri = 1; + string name = 2; + string creator_name = 3; + int64 add_time = 4; + + oneof entity { + YourLibraryRootlistPlaylist playlist = 5; + YourLibraryRootlistFolder folder = 6; + 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_request.proto b/protocol/proto/your_library_request.proto new file mode 100644 index 00000000..a75a0544 --- /dev/null +++ b/protocol/proto/your_library_request.proto @@ -0,0 +1,74 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +syntax = "proto3"; + +package spotify.your_library.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; +} diff --git a/protocol/proto/your_library_response.proto b/protocol/proto/your_library_response.proto new file mode 100644 index 00000000..124b35b4 --- /dev/null +++ b/protocol/proto/your_library_response.proto @@ -0,0 +1,112 @@ +// Extracted from: Spotify 1.1.61.583 (Windows) + +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; + } +} + +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; +} + +message YourLibraryResponse { + YourLibraryResponseHeader header = 1; + repeated YourLibraryResponseEntity entity = 2; + string error = 99; +} From 850db432540e1b8a7a33787372c4e2b4a22fb3bd Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 19 Jun 2021 22:47:39 +0200 Subject: [PATCH 005/147] Add token provider --- core/src/dealer/api.rs | 2 + core/src/lib.rs | 1 + core/src/session.rs | 10 ++++ core/src/token.rs | 124 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 core/src/dealer/api.rs create mode 100644 core/src/token.rs diff --git a/core/src/dealer/api.rs b/core/src/dealer/api.rs new file mode 100644 index 00000000..d9dd2b9b --- /dev/null +++ b/core/src/dealer/api.rs @@ -0,0 +1,2 @@ +// https://github.com/librespot-org/librespot-java/blob/27783e06f456f95228c5ac37acf2bff8c1a8a0c4/lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java + diff --git a/core/src/lib.rs b/core/src/lib.rs index f26caf3d..7a07d94d 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -25,6 +25,7 @@ mod proxytunnel; pub mod session; mod socket; pub mod spotify_id; +mod token; #[doc(hidden)] pub mod util; pub mod version; diff --git a/core/src/session.rs b/core/src/session.rs index 17452b20..fe8e4d5f 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -24,6 +24,7 @@ use crate::channel::ChannelManager; use crate::config::SessionConfig; use crate::connection::{self, AuthenticationError}; use crate::mercury::MercuryManager; +use crate::token::TokenProvider; #[derive(Debug, Error)] pub enum SessionError { @@ -49,6 +50,7 @@ struct SessionInternal { audio_key: OnceCell, channel: OnceCell, mercury: OnceCell, + token_provider: OnceCell, cache: Option>, handle: tokio::runtime::Handle, @@ -119,6 +121,7 @@ impl Session { audio_key: OnceCell::new(), channel: OnceCell::new(), mercury: OnceCell::new(), + token_provider: OnceCell::new(), handle, session_id, })); @@ -157,6 +160,12 @@ impl Session { .get_or_init(|| MercuryManager::new(self.weak())) } + pub fn token_provider(&self) -> &TokenProvider { + self.0 + .token_provider + .get_or_init(|| TokenProvider::new(self.weak())) + } + pub fn time_delta(&self) -> i64 { self.0.data.read().unwrap().time_delta } @@ -181,6 +190,7 @@ impl Session { #[allow(clippy::match_same_arms)] fn dispatch(&self, cmd: u8, data: Bytes) { match cmd { + // TODO: add command types 0x4 => { let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64; let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) { diff --git a/core/src/token.rs b/core/src/token.rs new file mode 100644 index 00000000..239b40af --- /dev/null +++ b/core/src/token.rs @@ -0,0 +1,124 @@ +// Ported from librespot-java. Relicensed under MIT with permission. + +use crate::mercury::MercuryError; + +use serde::Deserialize; + +use std::error::Error; +use std::time::{Duration, Instant}; + +component! { + TokenProvider : TokenProviderInner { + tokens: Vec = vec![], + } +} + +#[derive(Clone, Debug)] +pub struct Token { + expires_in: Duration, + access_token: String, + scopes: Vec, + timestamp: Instant, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenData { + expires_in: u64, + access_token: String, + scope: Vec, +} + +impl TokenProvider { + const KEYMASTER_CLIENT_ID: &'static str = "65b708073fc0480ea92a077233ca87bd"; + + fn find_token(&self, scopes: Vec) -> Option { + self.lock(|inner| { + for i in 0..inner.tokens.len() { + if inner.tokens[i].in_scopes(scopes.clone()) { + return Some(i); + } + } + None + }) + } + + pub async fn get_token(&self, scopes: Vec) -> Result { + if scopes.is_empty() { + return Err(MercuryError); + } + + if let Some(index) = self.find_token(scopes.clone()) { + let cached_token = self.lock(|inner| inner.tokens[index].clone()); + if cached_token.is_expired() { + self.lock(|inner| inner.tokens.remove(index)); + } else { + return Ok(cached_token); + } + } + + trace!( + "Requested token in scopes {:?} unavailable or expired, requesting new token.", + scopes + ); + + let query_uri = format!( + "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}", + scopes.join(","), + Self::KEYMASTER_CLIENT_ID, + self.session().device_id() + ); + let request = self.session().mercury().get(query_uri); + let response = request.await?; + + if response.status_code == 200 { + let data = response + .payload + .first() + .expect("No tokens received") + .to_vec(); + let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?; + trace!("Got token: {:?}", token); + self.lock(|inner| inner.tokens.push(token.clone())); + Ok(token) + } else { + Err(MercuryError) + } + } +} + +impl Token { + const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10); + + pub fn new(body: String) -> Result> { + let data: TokenData = serde_json::from_slice(body.as_ref())?; + Ok(Self { + expires_in: Duration::from_secs(data.expires_in), + access_token: data.access_token, + scopes: data.scope, + timestamp: Instant::now(), + }) + } + + pub fn is_expired(&self) -> bool { + self.timestamp + (self.expires_in - Self::EXPIRY_THRESHOLD) < Instant::now() + } + + pub fn in_scope(&self, scope: String) -> bool { + for s in &self.scopes { + if *s == scope { + return true; + } + } + false + } + + pub fn in_scopes(&self, scopes: Vec) -> bool { + for s in scopes { + if !self.in_scope(s) { + return false; + } + } + true + } +} From e1e265179fc2077a36919f235a4adb57bbad489e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 20 Jun 2021 20:40:33 +0200 Subject: [PATCH 006/147] Document known token scopes --- core/src/token.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/token.rs b/core/src/token.rs index 239b40af..0b95610d 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -1,5 +1,13 @@ // Ported from librespot-java. Relicensed under MIT with permission. +// Known tokens: +// ugc-image-upload, playlist-read-collaborative, playlist-modify-private, +// playlist-modify-public, playlist-read-private, user-read-playback-position, +// user-read-recently-played, user-top-read, user-modify-playback-state, +// user-read-currently-playing, user-read-playback-state, user-read-private, user-read-email, +// user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming, +// app-remote-control + use crate::mercury::MercuryError; use serde::Deserialize; From ce4f8dc288e167b6f37242e16fa83bb7ff0832a3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 20 Jun 2021 20:45:15 +0200 Subject: [PATCH 007/147] Remove superfluous status check --- core/src/token.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/core/src/token.rs b/core/src/token.rs index 0b95610d..3790bad7 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -78,20 +78,15 @@ impl TokenProvider { ); let request = self.session().mercury().get(query_uri); let response = request.await?; - - if response.status_code == 200 { - let data = response - .payload - .first() - .expect("No tokens received") - .to_vec(); - let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?; - trace!("Got token: {:?}", token); - self.lock(|inner| inner.tokens.push(token.clone())); - Ok(token) - } else { - Err(MercuryError) - } + let data = response + .payload + .first() + .expect("No tokens received") + .to_vec(); + let token = Token::new(String::from_utf8(data).unwrap()).map_err(|_| MercuryError)?; + trace!("Got token: {:?}", token); + self.lock(|inner| inner.tokens.push(token.clone())); + Ok(token) } } From 15628842af2859eb11d6530431f2639b9c1c5868 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 20 Jun 2021 23:09:27 +0200 Subject: [PATCH 008/147] Introduce HTTP client --- core/src/apresolve.rs | 38 ++++++++----------------- core/src/http_client.rs | 34 ++++++++++++++++++++++ core/src/lib.rs | 2 ++ core/src/session.rs | 16 +++++++---- core/src/{dealer/api.rs => spclient.rs} | 1 - 5 files changed, 58 insertions(+), 33 deletions(-) create mode 100644 core/src/http_client.rs rename core/src/{dealer/api.rs => spclient.rs} (99%) diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 975e0e18..55e9dc23 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,10 +1,7 @@ -use std::error::Error; - -use hyper::client::HttpConnector; -use hyper::{Body, Client, Request}; -use hyper_proxy::{Intercept, Proxy, ProxyConnector}; +use crate::http_client::HttpClient; +use hyper::{Body, Request}; use serde::Deserialize; -use url::Url; +use std::error::Error; const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient"; @@ -56,35 +53,21 @@ fn select_ap(data: Vec, fallback: &str, ap_port: Option) -> SocketA ap.unwrap_or_else(|| (String::from(fallback), port)) } -async fn try_apresolve(proxy: Option<&Url>) -> Result> { +async fn try_apresolve(http_client: &HttpClient) -> Result> { let req = Request::builder() .method("GET") .uri(APRESOLVE_ENDPOINT) .body(Body::empty()) .unwrap(); - let response = if let Some(url) = proxy { - // Panic safety: all URLs are valid URIs - let uri = url.to_string().parse().unwrap(); - 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? - } else { - Client::new().request(req).await? - }; - - let body = hyper::body::to_bytes(response.into_body()).await?; + let body = http_client.request_body(req).await?; let data: ApResolveData = serde_json::from_slice(body.as_ref())?; Ok(data) } -pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> AccessPoints { - let data = try_apresolve(proxy).await.unwrap_or_else(|e| { +pub async fn apresolve(http_client: &HttpClient, ap_port: Option) -> AccessPoints { + let data = try_apresolve(http_client).await.unwrap_or_else(|e| { warn!("Failed to resolve access points: {}, using fallbacks.", e); ApResolveData::default() }); @@ -105,10 +88,12 @@ mod test { use std::net::ToSocketAddrs; use super::apresolve; + use crate::http_client::HttpClient; #[tokio::test] async fn test_apresolve() { - let aps = apresolve(None, None).await; + let http_client = HttpClient::new(None); + let aps = apresolve(&http_client, None).await; // Assert that the result contains a valid host and port aps.accesspoint.to_socket_addrs().unwrap().next().unwrap(); @@ -118,7 +103,8 @@ mod test { #[tokio::test] async fn test_apresolve_port_443() { - let aps = apresolve(None, Some(443)).await; + let http_client = HttpClient::new(None); + let aps = apresolve(&http_client, Some(443)).await; let port = aps .accesspoint diff --git a/core/src/http_client.rs b/core/src/http_client.rs new file mode 100644 index 00000000..5f8ef780 --- /dev/null +++ b/core/src/http_client.rs @@ -0,0 +1,34 @@ +use hyper::client::HttpConnector; +use hyper::{Body, Client, Request, Response}; +use hyper_proxy::{Intercept, Proxy, ProxyConnector}; +use url::Url; + +pub struct HttpClient { + proxy: Option, +} + +impl HttpClient { + pub fn new(proxy: Option<&Url>) -> Self { + Self { + proxy: proxy.cloned(), + } + } + + 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(); + 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 + } else { + Client::new().request(req).await + } + } + + 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/lib.rs b/core/src/lib.rs index 7a07d94d..9e7b806d 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -19,11 +19,13 @@ mod connection; mod dealer; #[doc(hidden)] pub mod diffie_hellman; +mod http_client; pub mod keymaster; pub mod mercury; mod proxytunnel; pub mod session; mod socket; +mod spclient; pub mod spotify_id; mod token; #[doc(hidden)] diff --git a/core/src/session.rs b/core/src/session.rs index fe8e4d5f..8bf78f50 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -23,6 +23,7 @@ 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::token::TokenProvider; @@ -45,6 +46,7 @@ struct SessionInternal { config: SessionConfig, data: RwLock, + http_client: HttpClient, tx_connection: mpsc::UnboundedSender<(u8, Vec)>, audio_key: OnceCell, @@ -69,22 +71,22 @@ impl Session { credentials: Credentials, cache: Option, ) -> Result { - let ap = apresolve(config.proxy.as_ref(), config.ap_port) - .await - .accesspoint; + let http_client = HttpClient::new(config.proxy.as_ref()); + let ap = apresolve(&http_client, config.ap_port).await.accesspoint; info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); - let mut conn = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?; + let mut transport = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?; let reusable_credentials = - connection::authenticate(&mut conn, credentials, &config.device_id).await?; + connection::authenticate(&mut transport, credentials, &config.device_id).await?; info!("Authenticated as \"{}\" !", reusable_credentials.username); if let Some(cache) = &cache { cache.save_credentials(&reusable_credentials); } let session = Session::create( - conn, + transport, + http_client, config, cache, reusable_credentials.username, @@ -96,6 +98,7 @@ impl Session { fn create( transport: connection::Transport, + http_client: HttpClient, config: SessionConfig, cache: Option, username: String, @@ -116,6 +119,7 @@ impl Session { invalid: false, time_delta: 0, }), + http_client, tx_connection: sender_tx, cache: cache.map(Arc::new), audio_key: OnceCell::new(), diff --git a/core/src/dealer/api.rs b/core/src/spclient.rs similarity index 99% rename from core/src/dealer/api.rs rename to core/src/spclient.rs index d9dd2b9b..eb7b3f0f 100644 --- a/core/src/dealer/api.rs +++ b/core/src/spclient.rs @@ -1,2 +1 @@ // https://github.com/librespot-org/librespot-java/blob/27783e06f456f95228c5ac37acf2bff8c1a8a0c4/lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java - From b6357a27a5babf79825446b8fd2147183f61c507 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 21 Jun 2021 19:52:44 +0200 Subject: [PATCH 009/147] Store `token_type` and simplify `scopes` argument --- core/src/token.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/core/src/token.rs b/core/src/token.rs index 3790bad7..cce8718c 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -23,8 +23,9 @@ component! { #[derive(Clone, Debug)] pub struct Token { - expires_in: Duration, access_token: String, + expires_in: Duration, + token_type: String, scopes: Vec, timestamp: Instant, } @@ -32,15 +33,16 @@ pub struct Token { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct TokenData { - expires_in: u64, access_token: String, + expires_in: u64, + token_type: String, scope: Vec, } impl TokenProvider { const KEYMASTER_CLIENT_ID: &'static str = "65b708073fc0480ea92a077233ca87bd"; - fn find_token(&self, scopes: Vec) -> Option { + fn find_token(&self, scopes: Vec<&str>) -> Option { self.lock(|inner| { for i in 0..inner.tokens.len() { if inner.tokens[i].in_scopes(scopes.clone()) { @@ -51,12 +53,13 @@ impl TokenProvider { }) } - pub async fn get_token(&self, scopes: Vec) -> Result { + // scopes must be comma-separated + pub async fn get_token(&self, scopes: &str) -> Result { if scopes.is_empty() { return Err(MercuryError); } - if let Some(index) = self.find_token(scopes.clone()) { + 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() { self.lock(|inner| inner.tokens.remove(index)); @@ -72,7 +75,7 @@ impl TokenProvider { let query_uri = format!( "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}", - scopes.join(","), + scopes, Self::KEYMASTER_CLIENT_ID, self.session().device_id() ); @@ -96,8 +99,9 @@ impl Token { pub fn new(body: String) -> Result> { let data: TokenData = serde_json::from_slice(body.as_ref())?; Ok(Self { - expires_in: Duration::from_secs(data.expires_in), access_token: data.access_token, + expires_in: Duration::from_secs(data.expires_in), + token_type: data.token_type, scopes: data.scope, timestamp: Instant::now(), }) @@ -107,7 +111,7 @@ impl Token { self.timestamp + (self.expires_in - Self::EXPIRY_THRESHOLD) < Instant::now() } - pub fn in_scope(&self, scope: String) -> bool { + pub fn in_scope(&self, scope: &str) -> bool { for s in &self.scopes { if *s == scope { return true; @@ -116,7 +120,7 @@ impl Token { false } - pub fn in_scopes(&self, scopes: Vec) -> bool { + pub fn in_scopes(&self, scopes: Vec<&str>) -> bool { for s in scopes { if !self.in_scope(s) { return false; From eee79f2a1e610feefbaaae7f1a87467b95c705a4 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 21 Jun 2021 23:49:37 +0200 Subject: [PATCH 010/147] Introduce caching `ApResolver` component --- core/src/apresolve.rs | 208 ++++++++++++++++++++++-------------------- core/src/session.rs | 71 +++++++------- 2 files changed, 139 insertions(+), 140 deletions(-) diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 55e9dc23..68070106 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,118 +1,124 @@ -use crate::http_client::HttpClient; use hyper::{Body, Request}; use serde::Deserialize; use std::error::Error; -const APRESOLVE_ENDPOINT: &str = - "http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient"; - -// These addresses probably do some geo-location based traffic management or at least DNS-based -// load balancing. They are known to fail when the normal resolvers are up, so that's why they -// should only be used as fallback. -const AP_FALLBACK: &str = "ap.spotify.com"; -const DEALER_FALLBACK: &str = "dealer.spotify.com"; -const SPCLIENT_FALLBACK: &str = "spclient.wg.spotify.com"; - -const FALLBACK_PORT: u16 = 443; - pub type SocketAddress = (String, u16); -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Default)] +struct AccessPoints { + accesspoint: Vec, + dealer: Vec, + spclient: Vec, +} + +#[derive(Deserialize)] struct ApResolveData { accesspoint: Vec, dealer: Vec, spclient: Vec, } -#[derive(Clone, Debug, Deserialize)] -pub struct AccessPoints { - pub accesspoint: SocketAddress, - pub dealer: SocketAddress, - pub spclient: SocketAddress, -} - -fn select_ap(data: Vec, fallback: &str, ap_port: Option) -> SocketAddress { - let port = ap_port.unwrap_or(FALLBACK_PORT); - - let mut aps = data.into_iter().filter_map(|ap| { - let mut split = ap.rsplitn(2, ':'); - let port = split - .next() - .expect("rsplitn should not return empty iterator"); - let host = split.next()?.to_owned(); - let port: u16 = port.parse().ok()?; - Some((host, port)) - }); - - let ap = if ap_port.is_some() { - aps.find(|(_, p)| *p == port) - } else { - aps.next() - }; - - ap.unwrap_or_else(|| (String::from(fallback), port)) -} - -async fn try_apresolve(http_client: &HttpClient) -> Result> { - let req = Request::builder() - .method("GET") - .uri(APRESOLVE_ENDPOINT) - .body(Body::empty()) - .unwrap(); - - let body = http_client.request_body(req).await?; - let data: ApResolveData = serde_json::from_slice(body.as_ref())?; - - Ok(data) -} - -pub async fn apresolve(http_client: &HttpClient, ap_port: Option) -> AccessPoints { - let data = try_apresolve(http_client).await.unwrap_or_else(|e| { - warn!("Failed to resolve access points: {}, using fallbacks.", e); - ApResolveData::default() - }); - - let accesspoint = select_ap(data.accesspoint, AP_FALLBACK, ap_port); - let dealer = select_ap(data.dealer, DEALER_FALLBACK, ap_port); - let spclient = select_ap(data.spclient, SPCLIENT_FALLBACK, ap_port); - - AccessPoints { - accesspoint, - dealer, - spclient, +// These addresses probably do some geo-location based traffic management or at least DNS-based +// load balancing. They are known to fail when the normal resolvers are up, so that's why they +// should only be used as fallback. +impl Default for ApResolveData { + fn default() -> Self { + Self { + accesspoint: vec![String::from("ap.spotify.com:443")], + dealer: vec![String::from("dealer.spotify.com:443")], + spclient: vec![String::from("spclient.wg.spotify.com:443")], + } } } -#[cfg(test)] -mod test { - use std::net::ToSocketAddrs; - - use super::apresolve; - use crate::http_client::HttpClient; - - #[tokio::test] - async fn test_apresolve() { - let http_client = HttpClient::new(None); - let aps = apresolve(&http_client, None).await; - - // Assert that the result contains a valid host and port - aps.accesspoint.to_socket_addrs().unwrap().next().unwrap(); - aps.dealer.to_socket_addrs().unwrap().next().unwrap(); - aps.spclient.to_socket_addrs().unwrap().next().unwrap(); - } - - #[tokio::test] - async fn test_apresolve_port_443() { - let http_client = HttpClient::new(None); - let aps = apresolve(&http_client, Some(443)).await; - - let port = aps - .accesspoint - .to_socket_addrs() - .unwrap() - .next() - .unwrap() - .port(); - assert_eq!(port, 443); +component! { + ApResolver : ApResolverInner { + data: AccessPoints = AccessPoints::default(), + } +} + +impl ApResolver { + fn split_aps(data: Vec) -> Vec { + data.into_iter() + .filter_map(|ap| { + let mut split = ap.rsplitn(2, ':'); + let port = split + .next() + .expect("rsplitn should not return empty iterator"); + let host = split.next()?.to_owned(); + let port: u16 = port.parse().ok()?; + Some((host, port)) + }) + .collect() + } + + fn find_ap(&self, data: &[SocketAddress]) -> usize { + match self.session().config().proxy { + Some(_) => data + .iter() + .position(|(_, port)| *port == self.session().config().ap_port.unwrap_or(443)) + .expect("No access points available with that proxy port."), + None => 0, // just pick the first one + } + } + + 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(); + + let body = self.session().http_client().request_body(req).await?; + let data: ApResolveData = serde_json::from_slice(body.as_ref())?; + + Ok(data) + } + + async fn apresolve(&self) { + let result = self.try_apresolve().await; + self.lock(|inner| { + let data = match result { + Ok(data) => data, + Err(e) => { + warn!("Failed to resolve access points, using fallbacks: {}", e); + ApResolveData::default() + } + }; + + inner.data.accesspoint = Self::split_aps(data.accesspoint); + inner.data.dealer = Self::split_aps(data.dealer); + inner.data.spclient = Self::split_aps(data.spclient); + }) + } + + fn is_empty(&self) -> bool { + self.lock(|inner| { + inner.data.accesspoint.is_empty() + || inner.data.dealer.is_empty() + || inner.data.spclient.is_empty() + }) + } + + pub async fn resolve(&self, endpoint: &str) -> SocketAddress { + if self.is_empty() { + self.apresolve().await; + } + + self.lock(|inner| match endpoint { + "accesspoint" => { + let pos = self.find_ap(&inner.data.accesspoint); + inner.data.accesspoint.remove(pos) + } + "dealer" => { + let pos = self.find_ap(&inner.data.dealer); + inner.data.dealer.remove(pos) + } + "spclient" => { + let pos = self.find_ap(&inner.data.spclient); + inner.data.spclient.remove(pos) + } + _ => unimplemented!(), + }) } } diff --git a/core/src/session.rs b/core/src/session.rs index 8bf78f50..2f6e5703 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -16,7 +16,7 @@ use thiserror::Error; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; -use crate::apresolve::apresolve; +use crate::apresolve::ApResolver; use crate::audio_key::AudioKeyManager; use crate::authentication::Credentials; use crate::cache::Cache; @@ -49,6 +49,7 @@ struct SessionInternal { http_client: HttpClient, tx_connection: mpsc::UnboundedSender<(u8, Vec)>, + apresolver: OnceCell, audio_key: OnceCell, channel: OnceCell, mercury: OnceCell, @@ -72,40 +73,6 @@ impl Session { cache: Option, ) -> Result { let http_client = HttpClient::new(config.proxy.as_ref()); - let ap = apresolve(&http_client, config.ap_port).await.accesspoint; - - info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); - let mut transport = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?; - - let reusable_credentials = - connection::authenticate(&mut transport, credentials, &config.device_id).await?; - info!("Authenticated as \"{}\" !", reusable_credentials.username); - if let Some(cache) = &cache { - cache.save_credentials(&reusable_credentials); - } - - let session = Session::create( - transport, - http_client, - config, - cache, - reusable_credentials.username, - tokio::runtime::Handle::current(), - ); - - Ok(session) - } - - fn create( - transport: connection::Transport, - http_client: HttpClient, - config: SessionConfig, - cache: Option, - username: String, - handle: tokio::runtime::Handle, - ) -> Session { - let (sink, stream) = transport.split(); - let (sender_tx, sender_rx) = mpsc::unbounded_channel(); let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); @@ -115,21 +82,37 @@ impl Session { config, data: RwLock::new(SessionData { country: String::new(), - canonical_username: username, + canonical_username: String::new(), invalid: false, time_delta: 0, }), http_client, tx_connection: sender_tx, cache: cache.map(Arc::new), + apresolver: OnceCell::new(), audio_key: OnceCell::new(), channel: OnceCell::new(), mercury: OnceCell::new(), token_provider: OnceCell::new(), - handle, + handle: tokio::runtime::Handle::current(), session_id, })); + let ap = session.apresolver().resolve("accesspoint").await; + info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); + let mut transport = + connection::connect(&ap.0, ap.1, session.config().proxy.as_ref()).await?; + + let reusable_credentials = + 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(); + if let Some(cache) = session.cache() { + cache.save_credentials(&reusable_credentials); + } + + let (sink, stream) = transport.split(); let sender_task = UnboundedReceiverStream::new(sender_rx) .map(Ok) .forward(sink); @@ -143,7 +126,13 @@ impl Session { } }); - session + Ok(session) + } + + pub fn apresolver(&self) -> &ApResolver { + self.0 + .apresolver + .get_or_init(|| ApResolver::new(self.weak())) } pub fn audio_key(&self) -> &AudioKeyManager { @@ -158,6 +147,10 @@ impl Session { .get_or_init(|| ChannelManager::new(self.weak())) } + pub fn http_client(&self) -> &HttpClient { + &self.0.http_client + } + pub fn mercury(&self) -> &MercuryManager { self.0 .mercury @@ -230,7 +223,7 @@ impl Session { self.0.cache.as_ref() } - fn config(&self) -> &SessionConfig { + pub fn config(&self) -> &SessionConfig { &self.0.config } From 3a7843d049a87c8b1ec231537c37a8689e210173 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 22 Jun 2021 21:39:38 +0200 Subject: [PATCH 011/147] Fix refilling with proxies and a race condition --- core/src/apresolve.rs | 73 ++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 68070106..d82c3abc 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,6 +1,8 @@ use hyper::{Body, Request}; use serde::Deserialize; use std::error::Error; +use std::hint; +use std::sync::atomic::{AtomicUsize, Ordering}; pub type SocketAddress = (String, u16); @@ -34,11 +36,22 @@ impl Default for ApResolveData { component! { ApResolver : ApResolverInner { data: AccessPoints = AccessPoints::default(), + spinlock: AtomicUsize = AtomicUsize::new(0), } } impl ApResolver { - fn split_aps(data: Vec) -> Vec { + // 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 { + if self.session().config().proxy.is_some() || self.session().config().ap_port.is_some() { + Some(self.session().config().ap_port.unwrap_or(443)) + } else { + None + } + } + + fn process_data(&self, data: Vec) -> Vec { data.into_iter() .filter_map(|ap| { let mut split = ap.rsplitn(2, ':'); @@ -47,21 +60,16 @@ impl ApResolver { .expect("rsplitn should not return empty iterator"); let host = split.next()?.to_owned(); let port: u16 = port.parse().ok()?; + if let Some(p) = self.port_config() { + if p != port { + return None; + } + } Some((host, port)) }) .collect() } - fn find_ap(&self, data: &[SocketAddress]) -> usize { - match self.session().config().proxy { - Some(_) => data - .iter() - .position(|(_, port)| *port == self.session().config().ap_port.unwrap_or(443)) - .expect("No access points available with that proxy port."), - None => 0, // just pick the first one - } - } - async fn try_apresolve(&self) -> Result> { let req = Request::builder() .method("GET") @@ -77,6 +85,7 @@ impl ApResolver { async fn apresolve(&self) { let result = self.try_apresolve().await; + self.lock(|inner| { let data = match result { Ok(data) => data, @@ -86,9 +95,9 @@ impl ApResolver { } }; - inner.data.accesspoint = Self::split_aps(data.accesspoint); - inner.data.dealer = Self::split_aps(data.dealer); - inner.data.spclient = Self::split_aps(data.spclient); + inner.data.accesspoint = self.process_data(data.accesspoint); + inner.data.dealer = self.process_data(data.dealer); + inner.data.spclient = self.process_data(data.spclient); }) } @@ -101,24 +110,32 @@ impl ApResolver { } pub async fn resolve(&self, endpoint: &str) -> SocketAddress { + // Use a spinlock to make this function atomic. Otherwise, various race conditions may + // occur, e.g. when the session is created, multiple components are launched almost in + // parallel and they will all call this function, while resolving is still in progress. + self.lock(|inner| { + while inner.spinlock.load(Ordering::SeqCst) != 0 { + hint::spin_loop() + } + inner.spinlock.store(1, Ordering::SeqCst); + }); + if self.is_empty() { self.apresolve().await; } - self.lock(|inner| match endpoint { - "accesspoint" => { - let pos = self.find_ap(&inner.data.accesspoint); - inner.data.accesspoint.remove(pos) - } - "dealer" => { - let pos = self.find_ap(&inner.data.dealer); - inner.data.dealer.remove(pos) - } - "spclient" => { - let pos = self.find_ap(&inner.data.spclient); - inner.data.spclient.remove(pos) - } - _ => unimplemented!(), + self.lock(|inner| { + let access_point = match endpoint { + // take the first position instead of the last with `pop`, because Spotify returns + // access points with ports 4070, 443 and 80 in order of preference from highest + // to lowest. + "accesspoint" => inner.data.accesspoint.remove(0), + "dealer" => inner.data.dealer.remove(0), + "spclient" => inner.data.spclient.remove(0), + _ => unimplemented!(), + }; + inner.spinlock.store(0, Ordering::SeqCst); + access_point }) } } From d3074f597a88bb62b615d2b74f3e54e353573a04 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 22 Jun 2021 21:49:36 +0200 Subject: [PATCH 012/147] Remove `keymaster` and update example --- core/src/keymaster.rs | 26 -------------------------- core/src/lib.rs | 1 - examples/get_token.rs | 9 +++------ 3 files changed, 3 insertions(+), 33 deletions(-) delete mode 100644 core/src/keymaster.rs diff --git a/core/src/keymaster.rs b/core/src/keymaster.rs deleted file mode 100644 index 8c3c00a2..00000000 --- a/core/src/keymaster.rs +++ /dev/null @@ -1,26 +0,0 @@ -use serde::Deserialize; - -use crate::{mercury::MercuryError, session::Session}; - -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Token { - pub access_token: String, - pub expires_in: u32, - pub token_type: String, - pub scope: Vec, -} - -pub async fn get_token( - session: &Session, - client_id: &str, - scopes: &str, -) -> Result { - let url = format!( - "hm://keymaster/token/authenticated?client_id={}&scope={}", - client_id, scopes - ); - let response = session.mercury().get(url).await?; - let data = response.payload.first().expect("Empty payload"); - serde_json::from_slice(data.as_ref()).map_err(|_| MercuryError) -} diff --git a/core/src/lib.rs b/core/src/lib.rs index 9e7b806d..32ee0013 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -20,7 +20,6 @@ mod dealer; #[doc(hidden)] pub mod diffie_hellman; mod http_client; -pub mod keymaster; pub mod mercury; mod proxytunnel; pub mod session; diff --git a/examples/get_token.rs b/examples/get_token.rs index 636155e0..3ef6bd71 100644 --- a/examples/get_token.rs +++ b/examples/get_token.rs @@ -2,7 +2,6 @@ use std::env; use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; -use librespot::core::keymaster; use librespot::core::session::Session; const SCOPES: &str = @@ -13,8 +12,8 @@ async fn main() { let session_config = SessionConfig::default(); let args: Vec<_> = env::args().collect(); - if args.len() != 4 { - eprintln!("Usage: {} USERNAME PASSWORD CLIENT_ID", args[0]); + if args.len() != 3 { + eprintln!("Usage: {} USERNAME PASSWORD", args[0]); return; } @@ -26,8 +25,6 @@ async fn main() { println!( "Token: {:#?}", - keymaster::get_token(&session, &args[3], SCOPES) - .await - .unwrap() + session.token_provider().get_token(SCOPES).await.unwrap() ); } From 4fe1183a8050bd1e768e625e47d3a5ff0254e262 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 22 Jun 2021 21:54:50 +0200 Subject: [PATCH 013/147] Fix compilation on Rust 1.48 --- core/src/apresolve.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index d82c3abc..623c7cb3 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,7 +1,6 @@ use hyper::{Body, Request}; use serde::Deserialize; use std::error::Error; -use std::hint; use std::sync::atomic::{AtomicUsize, Ordering}; pub type SocketAddress = (String, u16); @@ -115,7 +114,8 @@ impl ApResolver { // parallel and they will all call this function, while resolving is still in progress. self.lock(|inner| { while inner.spinlock.load(Ordering::SeqCst) != 0 { - hint::spin_loop() + #[allow(deprecated)] + std::sync::atomic::spin_loop_hint() } inner.spinlock.store(1, Ordering::SeqCst); }); From 0703630041ac71dac299ad551e79ba04811f8b7a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 22 Jun 2021 23:57:38 +0200 Subject: [PATCH 014/147] Use `PacketType` instead of hex identifiers --- Cargo.lock | 50 +++++++++++++++++++++++++++++++++++++- audio/src/fetch/receive.rs | 3 ++- core/Cargo.toml | 2 ++ core/src/audio_key.rs | 9 ++++--- core/src/channel.rs | 9 ++++--- core/src/connection/mod.rs | 13 ++++++---- core/src/lib.rs | 2 ++ core/src/mercury/mod.rs | 24 ++++++++++-------- core/src/mercury/types.rs | 10 +++++--- core/src/packet.rs | 37 ++++++++++++++++++++++++++++ core/src/session.rs | 40 ++++++++++++++++++++---------- metadata/src/cover.rs | 3 ++- 12 files changed, 160 insertions(+), 42 deletions(-) create mode 100644 core/src/packet.rs diff --git a/Cargo.lock b/Cargo.lock index 1f97d578..176655b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,7 +635,7 @@ dependencies = [ "gstreamer-sys", "libc", "muldiv", - "num-rational", + "num-rational 0.3.2", "once_cell", "paste", "pretty-hex", @@ -1223,7 +1223,9 @@ dependencies = [ "hyper-proxy", "librespot-protocol", "log", + "num", "num-bigint", + "num-derive", "num-integer", "num-traits", "once_cell", @@ -1475,6 +1477,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational 0.4.0", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.0" @@ -1487,6 +1503,15 @@ dependencies = [ "rand", ] +[[package]] +name = "num-complex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -1508,6 +1533,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.3.2" @@ -1519,6 +1555,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 0f056c96..304c0e79 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -7,6 +7,7 @@ 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; @@ -46,7 +47,7 @@ pub fn request_range(session: &Session, file: FileId, offset: usize, length: usi data.write_u32::(start as u32).unwrap(); data.write_u32::(end as u32).unwrap(); - session.send_packet(0x8, data); + session.send_packet(PacketType::StreamChunk, data); channel } diff --git a/core/Cargo.toml b/core/Cargo.toml index 7eb4051c..3c239034 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -26,7 +26,9 @@ http = "0.2" hyper = { version = "0.14", features = ["client", "tcp", "http1"] } hyper-proxy = { version = "0.9.1", default-features = false } log = "0.4" +num = "0.4" num-bigint = { version = "0.4", features = ["rand"] } +num-derive = "0.3" num-integer = "0.1" num-traits = "0.2" once_cell = "1.5.2" diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index 3bce1c73..aae268e6 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::io::Write; use tokio::sync::oneshot; +use crate::packet::PacketType; use crate::spotify_id::{FileId, SpotifyId}; use crate::util::SeqGenerator; @@ -21,19 +22,19 @@ component! { } impl AudioKeyManager { - pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { let seq = BigEndian::read_u32(data.split_to(4).as_ref()); let sender = self.lock(|inner| inner.pending.remove(&seq)); if let Some(sender) = sender { match cmd { - 0xd => { + PacketType::AesKey => { let mut key = [0u8; 16]; key.copy_from_slice(data.as_ref()); let _ = sender.send(Ok(AudioKey(key))); } - 0xe => { + PacketType::AesKeyError => { warn!( "error audio key {:x} {:x}", data.as_ref()[0], @@ -66,6 +67,6 @@ impl AudioKeyManager { data.write_u32::(seq).unwrap(); data.write_u16::(0x0000).unwrap(); - self.session().send_packet(0xc, data) + self.session().send_packet(PacketType::RequestKey, data) } } diff --git a/core/src/channel.rs b/core/src/channel.rs index 4a78a4aa..4461612e 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -8,8 +8,10 @@ use bytes::Bytes; use futures_core::Stream; use futures_util::lock::BiLock; use futures_util::{ready, StreamExt}; +use num_traits::FromPrimitive; use tokio::sync::mpsc; +use crate::packet::PacketType; use crate::util::SeqGenerator; component! { @@ -66,7 +68,7 @@ impl ChannelManager { (seq, channel) } - pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { use std::collections::hash_map::Entry; let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref()); @@ -87,7 +89,7 @@ impl ChannelManager { inner.download_measurement_bytes += data.len(); if let Entry::Occupied(entry) = inner.channels.entry(id) { - let _ = entry.get().send((cmd, data)); + let _ = entry.get().send((cmd as u8, data)); } }); } @@ -109,7 +111,8 @@ impl Channel { fn recv_packet(&mut self, cx: &mut Context<'_>) -> Poll> { let (cmd, packet) = ready!(self.receiver.poll_recv(cx)).ok_or(ChannelError)?; - if cmd == 0xa { + let packet_type = FromPrimitive::from_u8(cmd); + if let Some(PacketType::ChannelError) = packet_type { let code = BigEndian::read_u16(&packet.as_ref()[..2]); error!("channel error: {} {}", packet.len(), code); diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index bacdc653..472109e6 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -7,6 +7,7 @@ pub use self::handshake::handshake; use std::io::{self, ErrorKind}; use futures_util::{SinkExt, StreamExt}; +use num_traits::FromPrimitive; use protobuf::{self, Message, ProtobufError}; use thiserror::Error; use tokio::net::TcpStream; @@ -14,6 +15,7 @@ use tokio_util::codec::Framed; use url::Url; use crate::authentication::Credentials; +use crate::packet::PacketType; use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; use crate::version; @@ -95,13 +97,14 @@ pub async fn authenticate( .set_device_id(device_id.to_string()); packet.set_version_string(version::VERSION_STRING.to_string()); - let cmd = 0xab; + let cmd = PacketType::Login; let data = packet.write_to_bytes().unwrap(); - transport.send((cmd, data)).await?; + transport.send((cmd as u8, data)).await?; let (cmd, data) = transport.next().await.expect("EOF")?; - match cmd { - 0xac => { + let packet_type = FromPrimitive::from_u8(cmd); + match packet_type { + Some(PacketType::APWelcome) => { let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?; let reusable_credentials = Credentials { @@ -112,7 +115,7 @@ pub async fn authenticate( Ok(reusable_credentials) } - 0xad => { + Some(PacketType::AuthFailure) => { let error_data = APLoginFailed::parse_from_bytes(data.as_ref())?; Err(error_data.into()) } diff --git a/core/src/lib.rs b/core/src/lib.rs index 32ee0013..b0996993 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -2,6 +2,7 @@ #[macro_use] extern crate log; +extern crate num_derive; use librespot_protocol as protocol; @@ -21,6 +22,7 @@ mod dealer; pub mod diffie_hellman; mod http_client; pub mod mercury; +pub mod packet; mod proxytunnel; pub mod session; mod socket; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 57650087..6cf3519e 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -11,6 +11,7 @@ use futures_util::FutureExt; use protobuf::Message; use tokio::sync::{mpsc, oneshot}; +use crate::packet::PacketType; use crate::protocol; use crate::util::SeqGenerator; @@ -143,7 +144,7 @@ impl MercuryManager { } } - pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { + 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(); @@ -154,14 +155,17 @@ impl MercuryManager { let mut pending = match pending { Some(pending) => pending, - None if cmd == 0xb5 => MercuryPending { - parts: Vec::new(), - partial: None, - callback: None, - }, None => { - warn!("Ignore seq {:?} cmd {:x}", seq, cmd); - return; + if let PacketType::MercuryEvent = cmd { + MercuryPending { + parts: Vec::new(), + partial: None, + callback: None, + } + } else { + warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8); + return; + } } }; @@ -191,7 +195,7 @@ impl MercuryManager { data.split_to(size).as_ref().to_owned() } - fn complete_request(&self, cmd: u8, mut pending: MercuryPending) { + fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) { let header_data = pending.parts.remove(0); let header = protocol::mercury::Header::parse_from_bytes(&header_data).unwrap(); @@ -208,7 +212,7 @@ impl MercuryManager { if let Some(cb) = pending.callback { let _ = cb.send(Err(MercuryError)); } - } else if cmd == 0xb5 { + } else if let PacketType::MercuryEvent = cmd { self.lock(|inner| { let mut found = false; diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 402a954c..616225db 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -2,6 +2,7 @@ use byteorder::{BigEndian, WriteBytesExt}; use protobuf::Message; use std::io::Write; +use crate::packet::PacketType; use crate::protocol; #[derive(Debug, PartialEq, Eq)] @@ -43,11 +44,12 @@ impl ToString for MercuryMethod { } impl MercuryMethod { - pub fn command(&self) -> u8 { + pub fn command(&self) -> PacketType { + use PacketType::*; match *self { - MercuryMethod::Get | MercuryMethod::Send => 0xb2, - MercuryMethod::Sub => 0xb3, - MercuryMethod::Unsub => 0xb4, + MercuryMethod::Get | MercuryMethod::Send => MercuryReq, + MercuryMethod::Sub => MercurySub, + MercuryMethod::Unsub => MercuryUnsub, } } } diff --git a/core/src/packet.rs b/core/src/packet.rs new file mode 100644 index 00000000..81645145 --- /dev/null +++ b/core/src/packet.rs @@ -0,0 +1,37 @@ +// Ported from librespot-java. Relicensed under MIT with permission. + +use num_derive::{FromPrimitive, ToPrimitive}; + +#[derive(Debug, FromPrimitive, ToPrimitive)] +pub enum PacketType { + SecretBlock = 0x02, + Ping = 0x04, + StreamChunk = 0x08, + StreamChunkRes = 0x09, + ChannelError = 0x0a, + ChannelAbort = 0x0b, + RequestKey = 0x0c, + AesKey = 0x0d, + AesKeyError = 0x0e, + Image = 0x19, + CountryCode = 0x1b, + Pong = 0x49, + PongAck = 0x4a, + Pause = 0x4b, + ProductInfo = 0x50, + LegacyWelcome = 0x69, + LicenseVersion = 0x76, + Login = 0xab, + APWelcome = 0xac, + AuthFailure = 0xad, + MercuryReq = 0xb2, + MercurySub = 0xb3, + MercuryUnsub = 0xb4, + MercuryEvent = 0xb5, + TrackEndedTime = 0x82, + UnknownDataAllZeros = 0x1f, + PreferredLocale = 0x74, + Unknown0x4f = 0x4f, + Unknown0x0f = 0x0f, + Unknown0x10 = 0x10, +} diff --git a/core/src/session.rs b/core/src/session.rs index 2f6e5703..1bf62aa2 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -11,6 +11,7 @@ use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use futures_core::TryStream; use futures_util::{future, ready, StreamExt, TryStreamExt}; +use num_traits::FromPrimitive; use once_cell::sync::OnceCell; use thiserror::Error; use tokio::sync::mpsc; @@ -25,6 +26,7 @@ use crate::config::SessionConfig; use crate::connection::{self, AuthenticationError}; use crate::http_client::HttpClient; use crate::mercury::MercuryManager; +use crate::packet::PacketType; use crate::token::TokenProvider; #[derive(Debug, Error)] @@ -184,11 +186,11 @@ impl Session { ); } - #[allow(clippy::match_same_arms)] fn dispatch(&self, cmd: u8, data: Bytes) { - match cmd { - // TODO: add command types - 0x4 => { + use PacketType::*; + let packet_type = FromPrimitive::from_u8(cmd); + match packet_type { + Some(Ping) => { let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64; let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(dur) => dur, @@ -199,24 +201,36 @@ impl Session { self.0.data.write().unwrap().time_delta = server_timestamp - timestamp; self.debug_info(); - self.send_packet(0x49, vec![0, 0, 0, 0]); + self.send_packet(Pong, vec![0, 0, 0, 0]); } - 0x4a => (), - 0x1b => { + Some(PongAck) => {} + Some(CountryCode) => { let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); info!("Country: {:?}", country); self.0.data.write().unwrap().country = country; } - 0x9 | 0xa => self.channel().dispatch(cmd, data), - 0xd | 0xe => self.audio_key().dispatch(cmd, data), - 0xb2..=0xb6 => self.mercury().dispatch(cmd, data), - _ => (), + Some(StreamChunkRes) | Some(ChannelError) => { + self.channel().dispatch(packet_type.unwrap(), data) + } + Some(AesKey) | Some(AesKeyError) => { + self.audio_key().dispatch(packet_type.unwrap(), data) + } + Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => { + self.mercury().dispatch(packet_type.unwrap(), data) + } + _ => { + if let Some(packet_type) = PacketType::from_u8(cmd) { + trace!("Ignoring {:?} packet", packet_type); + } else { + trace!("Ignoring unknown packet {:x}", cmd); + } + } } } - pub fn send_packet(&self, cmd: u8, data: Vec) { - self.0.tx_connection.send((cmd, data)).unwrap(); + pub fn send_packet(&self, cmd: PacketType, data: Vec) { + self.0.tx_connection.send((cmd as u8, data)).unwrap(); } pub fn cache(&self) -> Option<&Arc> { diff --git a/metadata/src/cover.rs b/metadata/src/cover.rs index 408e658e..b483f454 100644 --- a/metadata/src/cover.rs +++ b/metadata/src/cover.rs @@ -2,6 +2,7 @@ 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; @@ -13,7 +14,7 @@ pub fn get(session: &Session, file: FileId) -> ChannelData { packet.write_u16::(channel_id).unwrap(); packet.write_u16::(0).unwrap(); packet.write(&file.0).unwrap(); - session.send_packet(0x19, packet); + session.send_packet(PacketType::Image, packet); data } From 12365ae082b11d077adbbe9d5950fee444869a37 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 22 Jun 2021 23:58:35 +0200 Subject: [PATCH 015/147] Fix comment --- core/src/token.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/token.rs b/core/src/token.rs index cce8718c..824fcc3b 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -1,6 +1,6 @@ // Ported from librespot-java. Relicensed under MIT with permission. -// Known tokens: +// Known scopes: // ugc-image-upload, playlist-read-collaborative, playlist-modify-private, // playlist-modify-public, playlist-read-private, user-read-playback-position, // user-read-recently-played, user-top-read, user-modify-playback-state, From aa4cc0bee66b57efc52c6f93d676a3ad65b195fb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 23 Jun 2021 21:26:52 +0200 Subject: [PATCH 016/147] Ignore known but unused packets --- core/src/session.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/src/session.rs b/core/src/session.rs index 1bf62aa2..81975a80 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -203,25 +203,29 @@ impl Session { self.debug_info(); self.send_packet(Pong, vec![0, 0, 0, 0]); } - Some(PongAck) => {} Some(CountryCode) => { let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); info!("Country: {:?}", country); self.0.data.write().unwrap().country = country; } - Some(StreamChunkRes) | Some(ChannelError) => { - self.channel().dispatch(packet_type.unwrap(), data) + self.channel().dispatch(packet_type.unwrap(), data); } Some(AesKey) | Some(AesKeyError) => { - self.audio_key().dispatch(packet_type.unwrap(), data) + self.audio_key().dispatch(packet_type.unwrap(), data); } Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => { - self.mercury().dispatch(packet_type.unwrap(), data) + self.mercury().dispatch(packet_type.unwrap(), data); } + Some(PongAck) + | Some(SecretBlock) + | Some(LegacyWelcome) + | Some(UnknownDataAllZeros) + | Some(ProductInfo) + | Some(LicenseVersion) => {} _ => { if let Some(packet_type) = PacketType::from_u8(cmd) { - trace!("Ignoring {:?} packet", packet_type); + trace!("Ignoring {:?} packet with data {:?}", packet_type, data); } else { trace!("Ignoring unknown packet {:x}", cmd); } From e58934849f23c94e74e35e025d688b63841bfdbd Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 23 Jun 2021 21:43:23 +0200 Subject: [PATCH 017/147] Fix clippy warnings --- core/src/audio_key.rs | 4 ++-- core/src/lib.rs | 2 -- core/src/mercury/types.rs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index aae268e6..f42c6502 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -62,8 +62,8 @@ impl AudioKeyManager { fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) { let mut data: Vec = Vec::new(); - data.write(&file.0).unwrap(); - data.write(&track.to_raw()).unwrap(); + data.write_all(&file.0).unwrap(); + data.write_all(&track.to_raw()).unwrap(); data.write_u32::(seq).unwrap(); data.write_u16::(0x0000).unwrap(); diff --git a/core/src/lib.rs b/core/src/lib.rs index b0996993..9c92c235 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(clippy::unused_io_amount)] - #[macro_use] extern crate log; extern crate num_derive; diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 616225db..1d6b5b15 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -79,7 +79,7 @@ impl MercuryRequest { for p in &self.payload { packet.write_u16::(p.len() as u16).unwrap(); - packet.write(p).unwrap(); + packet.write_all(p).unwrap(); } packet From 7d27b94cfb6ba53e8e6a526c6895fad65e7f81d5 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 25 Jun 2021 23:56:17 +0200 Subject: [PATCH 018/147] Document new unknown packet 0xb6 --- core/src/packet.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/packet.rs b/core/src/packet.rs index 81645145..de780f13 100644 --- a/core/src/packet.rs +++ b/core/src/packet.rs @@ -31,7 +31,11 @@ pub enum PacketType { TrackEndedTime = 0x82, UnknownDataAllZeros = 0x1f, PreferredLocale = 0x74, - Unknown0x4f = 0x4f, Unknown0x0f = 0x0f, Unknown0x10 = 0x10, + Unknown0x4f = 0x4f, + + // TODO - occurs when subscribing with an empty URI. Maybe a MercuryError? + // Payload: b"\0\x08\0\0\0\0\0\0\0\0\x01\0\x01\0\x03 \xb0\x06" + Unknown0xb6 = 0xb6, } From 39bf40bcc7b9712c5f952689fee8d877a4bf6cf8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 28 Jun 2021 20:58:58 +0200 Subject: [PATCH 019/147] Lay groundwork for new Spotify API client (#805) Lay groundwork for new Spotify API before removing `spirc` * Add token provider * Introduce HTTP client * Introduce caching `ApResolver` component * Remove `keymaster` and update example * Use `PacketType` instead of hex identifiers * Document new unknown packet 0xb6 --- .github/workflows/test.yml | 14 +- CHANGELOG.md | 38 +- Cargo.lock | 280 ++++---- Cargo.toml | 12 +- README.md | 1 + audio/src/fetch/mod.rs | 136 ++-- audio/src/fetch/receive.rs | 73 +- audio/src/lib.rs | 4 +- connect/Cargo.toml | 21 +- connect/src/discovery.rs | 262 +------ connect/src/lib.rs | 4 + connect/src/spirc.rs | 97 +-- contrib/librespot.service | 7 +- contrib/librespot.user.service | 12 + core/Cargo.toml | 2 + core/src/apresolve.rs | 243 +++---- core/src/audio_key.rs | 13 +- core/src/channel.rs | 18 +- core/src/config.rs | 87 +-- core/src/connection/mod.rs | 13 +- core/src/http_client.rs | 34 + core/src/keymaster.rs | 26 - core/src/lib.rs | 8 +- core/src/mercury/mod.rs | 24 +- core/src/mercury/types.rs | 12 +- core/src/packet.rs | 41 ++ core/src/session.rs | 129 ++-- core/src/spclient.rs | 1 + core/src/spotify_id.rs | 33 +- core/src/token.rs | 131 ++++ discovery/Cargo.toml | 40 ++ discovery/examples/discovery.rs | 25 + discovery/src/lib.rs | 150 ++++ discovery/src/server.rs | 236 +++++++ examples/get_token.rs | 9 +- metadata/src/cover.rs | 3 +- playback/Cargo.toml | 19 +- playback/src/audio_backend/alsa.rs | 259 +++++-- playback/src/audio_backend/gstreamer.rs | 20 +- playback/src/audio_backend/jackaudio.rs | 16 +- playback/src/audio_backend/mod.rs | 70 +- playback/src/audio_backend/pipe.rs | 52 +- playback/src/audio_backend/portaudio.rs | 30 +- playback/src/audio_backend/pulseaudio.rs | 22 +- playback/src/audio_backend/rodio.rs | 27 +- playback/src/audio_backend/sdl.rs | 22 +- playback/src/audio_backend/subprocess.rs | 5 + playback/src/config.rs | 95 ++- playback/src/convert.rs | 151 ++-- playback/src/decoder/lewton_decoder.rs | 11 +- playback/src/decoder/libvorbis_decoder.rs | 89 --- playback/src/decoder/mod.rs | 22 +- playback/src/decoder/passthrough_decoder.rs | 9 +- playback/src/dither.rs | 150 ++++ playback/src/lib.rs | 5 + playback/src/mixer/alsamixer.rs | 440 ++++++------ playback/src/mixer/mappings.rs | 163 +++++ playback/src/mixer/mod.rs | 41 +- playback/src/mixer/softmixer.rs | 45 +- playback/src/player.rs | 207 +++--- src/lib.rs | 1 + src/main.rs | 728 ++++++++++++-------- 62 files changed, 3101 insertions(+), 1837 deletions(-) create mode 100644 contrib/librespot.user.service create mode 100644 core/src/http_client.rs delete mode 100644 core/src/keymaster.rs create mode 100644 core/src/packet.rs create mode 100644 core/src/spclient.rs create mode 100644 core/src/token.rs create mode 100644 discovery/Cargo.toml create mode 100644 discovery/examples/discovery.rs create mode 100644 discovery/src/lib.rs create mode 100644 discovery/src/server.rs delete mode 100644 playback/src/decoder/libvorbis_decoder.rs create mode 100644 playback/src/dither.rs create mode 100644 playback/src/mixer/mappings.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 825fc936..6e447ff9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,11 @@ on: "Cargo.lock", "rustfmt.toml", ".github/workflows/*", + "!*.md", + "!contrib/*", + "!docs/*", + "!LICENSE", + "!*.sh", ] pull_request: paths: @@ -20,6 +25,11 @@ on: "Cargo.lock", "rustfmt.toml", ".github/workflows/*", + "!*.md", + "!contrib/*", + "!docs/*", + "!LICENSE", + "!*.sh", ] schedule: # Run CI every week @@ -99,8 +109,8 @@ jobs: - run: cargo hack --workspace --remove-dev-deps - run: cargo build -p librespot-core --no-default-features - run: cargo build -p librespot-core - - run: cargo build -p librespot-connect - - run: cargo build -p librespot-connect --no-default-features --features with-dns-sd + - run: cargo hack build --each-feature -p librespot-discovery + - run: cargo hack build --each-feature -p librespot-playback - run: cargo hack build --each-feature test-windows: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a775d4c..ceb63541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,44 @@ 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] +### 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) + +### 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] 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 +- [playback] Store and pass samples in 64-bit floating point +- [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`: 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) + +### Deprecated +- [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate ### Removed - -* [librespot-audio] Removed `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot_audio`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. +- [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 ### Fixed - -* [librespot-playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream +- [connect] Fix step size on volume up/down events +- [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 +- [playback] `alsamixer`: make `cubic` consistent between cards that report minimum volume as mute, and cards that report some dB value +- [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 ## [0.2.0] - 2021-05-04 diff --git a/Cargo.lock b/Cargo.lock index 1f97d578..37cbae56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,5 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - [[package]] name = "aes" version = "0.6.0" @@ -170,9 +168,9 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" dependencies = [ "jobserver", ] @@ -237,6 +235,17 @@ dependencies = [ "libloading 0.7.0", ] +[[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.5.2" @@ -300,9 +309,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec1028182c380cc45a2e2c5ec841134f2dfd0f8f5f0a5bcd68004f81b5efdf4" +checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" dependencies = [ "libc", ] @@ -428,9 +437,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d5813545e459ad3ca1bff9915e9ad7f1a47dc6a91b627ce321d5863b7dd253" +checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" dependencies = [ "futures-channel", "futures-core", @@ -520,12 +529,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gcc" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" - [[package]] name = "generic-array" version = "0.14.4" @@ -547,9 +550,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if 1.0.0", "libc", @@ -635,7 +638,7 @@ dependencies = [ "gstreamer-sys", "libc", "muldiv", - "num-rational", + "num-rational 0.3.2", "once_cell", "paste", "pretty-hex", @@ -816,15 +819,15 @@ dependencies = [ [[package]] name = "httparse" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1ce40d6fc9764887c2fdc7305c3dcc429ba11ff981c1509416afd5697e4437" +checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" [[package]] name = "httpdate" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05842d0d43232b23ccb7060ecb0f0626922c21f30012e97b767b30afd4a5d4b9" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" [[package]] name = "humantime" @@ -834,9 +837,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.7" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e5f105c494081baa3bf9e200b279e27ec1623895cd504c7dbef8d0b080fcf54" +checksum = "d3f71a7eea53a3f8257a7b4795373ff886397178cd634430ea94e12d7fe4fe34" dependencies = [ "bytes", "futures-channel", @@ -1049,9 +1052,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.94" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" +checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" [[package]] name = "libloading" @@ -1073,6 +1076,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + [[package]] name = "libmdns" version = "0.6.1" @@ -1152,6 +1161,7 @@ dependencies = [ "librespot-audio", "librespot-connect", "librespot-core", + "librespot-discovery", "librespot-metadata", "librespot-playback", "librespot-protocol", @@ -1181,16 +1191,10 @@ dependencies = [ name = "librespot-connect" version = "0.2.0" dependencies = [ - "aes-ctr", - "base64", - "dns-sd", "form_urlencoded", - "futures-core", "futures-util", - "hmac", - "hyper", - "libmdns", "librespot-core", + "librespot-discovery", "librespot-playback", "librespot-protocol", "log", @@ -1198,10 +1202,8 @@ dependencies = [ "rand", "serde", "serde_json", - "sha-1", "tokio", "tokio-stream", - "url", ] [[package]] @@ -1223,7 +1225,9 @@ dependencies = [ "hyper-proxy", "librespot-protocol", "log", + "num", "num-bigint", + "num-derive", "num-integer", "num-traits", "once_cell", @@ -1245,6 +1249,31 @@ dependencies = [ "vergen", ] +[[package]] +name = "librespot-discovery" +version = "0.2.0" +dependencies = [ + "aes-ctr", + "base64", + "cfg-if 1.0.0", + "dns-sd", + "form_urlencoded", + "futures", + "futures-core", + "hex", + "hmac", + "hyper", + "libmdns", + "librespot-core", + "log", + "rand", + "serde_json", + "sha-1", + "simple_logger", + "thiserror", + "tokio", +] + [[package]] name = "librespot-metadata" version = "0.2.0" @@ -1263,7 +1292,6 @@ version = "0.2.0" dependencies = [ "alsa", "byteorder", - "cfg-if 1.0.0", "cpal", "futures-executor", "futures-util", @@ -1277,16 +1305,16 @@ dependencies = [ "librespot-audio", "librespot-core", "librespot-metadata", - "librespot-tremor", "log", "ogg", "portaudio-rs", + "rand", + "rand_distr", "rodio", "sdl2", "shell-words", "thiserror", "tokio", - "vorbis", "zerocopy", ] @@ -1299,18 +1327,6 @@ dependencies = [ "protobuf-codegen-pure", ] -[[package]] -name = "librespot-tremor" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97f525bff915d478a76940a7b988e5ea34911ba7280c97bd3a7673f54d68b4fe" -dependencies = [ - "cc", - "libc", - "ogg-sys", - "pkg-config", -] - [[package]] name = "lock_api" version = "0.4.4" @@ -1475,6 +1491,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational 0.4.0", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.0" @@ -1487,6 +1517,15 @@ dependencies = [ "rand", ] +[[package]] +name = "num-complex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -1508,6 +1547,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.3.2" @@ -1519,6 +1569,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -1526,6 +1588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1562,9 +1625,9 @@ dependencies = [ [[package]] name = "oboe" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfb2390bddb9546c0f7448fd1d2abdd39e6075206f960991eb28c7fa7f126c4" +checksum = "dfa187b38ae20374617b7ad418034ed3dc90ac980181d211518bd03537ae8f8d" dependencies = [ "jni", "ndk", @@ -1576,9 +1639,9 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe069264d082fc820dfa172f79be3f2e088ecfece9b1c47b0c9fd838d2bef103" +checksum = "b88e64835aa3f579c08d182526dc34e3907343d5b97e87b71a40ba5bca7aca9e" dependencies = [ "cc", ] @@ -1592,17 +1655,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "ogg-sys" -version = "0.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a95b8c172e17df1a41bf8d666301d3b2c4efeb90d9d0415e2a4dc0668b35fdb2" -dependencies = [ - "gcc", - "libc", - "pkg-config", -] - [[package]] name = "once_cell" version = "1.7.2" @@ -1805,9 +1857,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" dependencies = [ "unicode-xid", ] @@ -1877,6 +1929,16 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_distr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9e8f32ad24fb80d07d2323a9a2ce8b30d68a62b8cb4df88119ff49a698f038" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "rand_hc" version = "0.3.0" @@ -2057,18 +2119,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.125" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.125" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" dependencies = [ "proc-macro2", "quote", @@ -2129,6 +2191,19 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_logger" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd57f17c093ead1d4a1499dc9acaafdd71240908d64775465543b8d9a9f1d198" +dependencies = [ + "atty", + "chrono", + "colored", + "log", + "winapi", +] + [[package]] name = "slab" version = "0.4.3" @@ -2256,18 +2331,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" dependencies = [ "proc-macro2", "quote", @@ -2301,9 +2376,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5" +checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37" dependencies = [ "autocfg", "bytes", @@ -2320,9 +2395,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" +checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" dependencies = [ "proc-macro2", "quote", @@ -2342,9 +2417,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0" +checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" dependencies = [ "futures-core", "pin-project-lite", @@ -2370,9 +2445,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940a12c99365c31ea8dd9ba04ec1be183ffe4920102bb7122c2f515437601e8e" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" dependencies = [ "bytes", "futures-core", @@ -2550,43 +2625,6 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" -[[package]] -name = "vorbis" -version = "0.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e8a194457075360557b82dac78f7ca2d65bbb6679bccfabae5f7c8c706cc776" -dependencies = [ - "libc", - "ogg-sys", - "vorbis-sys", - "vorbisfile-sys", -] - -[[package]] -name = "vorbis-sys" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9ed6ef5361a85e68ccc005961d995c2d44e31f0816f142025f2ca2383dfbfd" -dependencies = [ - "cc", - "libc", - "ogg-sys", - "pkg-config", -] - -[[package]] -name = "vorbisfile-sys" -version = "0.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4306d7e1ac4699b55e20de9483750b90c250913188efd7484db6bfbe9042d1" -dependencies = [ - "gcc", - "libc", - "ogg-sys", - "pkg-config", - "vorbis-sys", -] - [[package]] name = "walkdir" version = "2.3.2" @@ -2670,9 +2708,9 @@ checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" [[package]] name = "web-sys" -version = "0.3.50" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" +checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 5df27872..ced7d0f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,10 @@ version = "0.2.0" path = "core" version = "0.2.0" +[dependencies.librespot-discovery] +path = "discovery" +version = "0.2.0" + [dependencies.librespot-metadata] path = "metadata" version = "0.2.0" @@ -68,10 +72,7 @@ rodiojack-backend = ["librespot-playback/rodiojack-backend"] sdl-backend = ["librespot-playback/sdl-backend"] gstreamer-backend = ["librespot-playback/gstreamer-backend"] -with-tremor = ["librespot-playback/with-tremor"] -with-vorbis = ["librespot-playback/with-vorbis"] - -with-dns-sd = ["librespot-connect/with-dns-sd"] +with-dns-sd = ["librespot-discovery/with-dns-sd"] default = ["rodio-backend"] @@ -89,5 +90,6 @@ section = "sound" priority = "optional" assets = [ ["target/release/librespot", "usr/bin/", "755"], - ["contrib/librespot.service", "lib/systemd/system/", "644"] + ["contrib/librespot.service", "lib/systemd/system/", "644"], + ["contrib/librespot.user.service", "lib/systemd/user/", "644"] ] diff --git a/README.md b/README.md index 33b2b76e..bcf73cac 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ The above command will create a receiver named ```Librespot```, with bitrate set A full list of runtime options are 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`._ + ## Contact Come and hang out on gitter if you need help or want to offer some. https://gitter.im/librespot-org/spotify-connect-resources diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 8e076ebc..636194a8 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -18,70 +18,70 @@ use tokio::sync::{mpsc, oneshot}; 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. +/// 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; -// 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. +/// 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; -// 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_PING_TIME_ESTIMATE_SECONDS: f64 = 0.5; -// The pig time that is used for calculations before a ping time was actually measured. +/// 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); -const MAXIMUM_ASSUMED_PING_TIME_SECONDS: f64 = 1.5; -// 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. +/// 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 READ_AHEAD_BEFORE_PLAYBACK_SECONDS: f64 = 1.0; -// 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 -// of audio data may be larger or smaller. +/// 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 +/// of audio data may be larger or smaller. +pub const READ_AHEAD_BEFORE_PLAYBACK: Duration = Duration::from_secs(1); -pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f64 = 2.0; -// Same as READ_AHEAD_BEFORE_PLAYBACK_SECONDS, but the time is taken as a factor of the ping -// time to the Spotify server. -// Both, READ_AHEAD_BEFORE_PLAYBACK_SECONDS and READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS are -// obeyed. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. +/// Same as `READ_AHEAD_BEFORE_PLAYBACK`, but the time is taken as a factor of the ping +/// time to the Spotify server. Both `READ_AHEAD_BEFORE_PLAYBACK` and +/// `READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS` are obeyed. +/// Note: the calculations are done using the nominal bitrate of the file. The actual amount +/// of audio data may be larger or smaller. +pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f32 = 2.0; -pub const READ_AHEAD_DURING_PLAYBACK_SECONDS: f64 = 5.0; -// While playing back, this many seconds of data ahead of the current read position are -// requested. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. +/// While playing back, this many seconds of data ahead of the current read position are +/// requested. +/// Note: the calculations are done using the nominal bitrate of the file. The actual amount +/// of audio data may be larger or smaller. +pub const READ_AHEAD_DURING_PLAYBACK: Duration = Duration::from_secs(5); -pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f64 = 10.0; -// Same as READ_AHEAD_DURING_PLAYBACK_SECONDS, but the time is taken as a factor of the ping -// time to the Spotify server. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. +/// Same as `READ_AHEAD_DURING_PLAYBACK`, but the time is taken as a factor of the ping +/// time to the Spotify server. +/// Note: the calculations are done using the nominal bitrate of the file. The actual amount +/// of audio data may be larger or smaller. +pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f32 = 10.0; -const PREFETCH_THRESHOLD_FACTOR: f64 = 4.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 * * +/// 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; -const FAST_PREFETCH_THRESHOLD_FACTOR: f64 = 1.5; -// Similar to PREFETCH_THRESHOLD_FACTOR, but it also takes the current download rate into account. -// The formula used is -// < FAST_PREFETCH_THRESHOLD_FACTOR * * -// This mechanism allows for fast downloading of the remainder of the file. The number should be larger -// than 1 so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster -// 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 cause the download rate to collapse and effectively -// only PREFETCH_THRESHOLD_FACTOR is in effect. Thus, set to zero if bandwidth saturation is not wanted. +/// Similar to `PREFETCH_THRESHOLD_FACTOR`, but it also takes the current download rate into account. +/// The formula used is ` < FAST_PREFETCH_THRESHOLD_FACTOR * * ` +/// This mechanism allows for fast downloading of the remainder of the file. The number should be larger +/// than `1.0` so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster +/// 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; +/// 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 +/// 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; -// 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 -// 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. + +/// The time we will wait to obtain status updates on downloading. +const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(1); pub enum AudioFile { Cached(fs::File), @@ -131,10 +131,10 @@ impl StreamLoaderController { }) } - pub fn ping_time_ms(&self) -> usize { - self.stream_shared.as_ref().map_or(0, |shared| { - shared.ping_time_ms.load(atomic::Ordering::Relaxed) - }) + pub fn ping_time(&self) -> Duration { + Duration::from_millis(self.stream_shared.as_ref().map_or(0, |shared| { + shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64 + })) } fn send_stream_loader_command(&self, command: StreamLoaderCommand) { @@ -170,7 +170,7 @@ impl StreamLoaderController { { download_status = shared .cond - .wait_timeout(download_status, Duration::from_millis(1000)) + .wait_timeout(download_status, DOWNLOAD_TIMEOUT) .unwrap() .0; if range.length @@ -271,10 +271,10 @@ impl AudioFile { let mut initial_data_length = if play_from_beginning { INITIAL_DOWNLOAD_SIZE + max( - (READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, - (INITIAL_PING_TIME_ESTIMATE_SECONDS + (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 f64) as usize, + * bytes_per_second as f32) as usize, ) } else { INITIAL_DOWNLOAD_SIZE @@ -368,7 +368,7 @@ impl AudioFileStreaming { let read_file = write_file.reopen().unwrap(); - //let (seek_tx, seek_rx) = mpsc::unbounded(); + // let (seek_tx, seek_rx) = mpsc::unbounded(); let (stream_loader_command_tx, stream_loader_command_rx) = mpsc::unbounded_channel::(); @@ -405,17 +405,19 @@ impl Read for AudioFileStreaming { 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 reqeust demanded. - let ping_time_seconds = - 0.0001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; + // Due to the read-ahead stuff, we potentially request more than the actual request demanded. + let ping_time_seconds = Duration::from_millis( + self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64, + ) + .as_secs_f32(); let length_to_request = length + max( - (READ_AHEAD_DURING_PLAYBACK_SECONDS * self.shared.stream_data_rate as f64) - as usize, + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() + * self.shared.stream_data_rate as f32) as usize, (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS * ping_time_seconds - * self.shared.stream_data_rate as f64) as usize, + * self.shared.stream_data_rate as f32) as usize, ); min(length_to_request, self.shared.file_size - offset) } @@ -449,7 +451,7 @@ impl Read for AudioFileStreaming { download_status = self .shared .cond - .wait_timeout(download_status, Duration::from_millis(1000)) + .wait_timeout(download_status, DOWNLOAD_TIMEOUT) .unwrap() .0; } diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 0f056c96..5de90b79 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -1,12 +1,14 @@ use std::cmp::{max, min}; use std::io::{Seek, SeekFrom, Write}; use std::sync::{atomic, Arc}; -use std::time::Instant; +use std::time::{Duration, Instant}; +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; @@ -16,7 +18,7 @@ use crate::range_set::{Range, RangeSet}; use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand}; use super::{ - FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME_SECONDS, MAX_PREFETCH_REQUESTS, + FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, }; @@ -46,7 +48,7 @@ pub fn request_range(session: &Session, file: FileId, offset: usize, length: usi data.write_u32::(start as u32).unwrap(); data.write_u32::(end as u32).unwrap(); - session.send_packet(0x8, data); + session.send_packet(PacketType::StreamChunk, data); channel } @@ -57,7 +59,7 @@ struct PartialFileData { } enum ReceivedData { - ResponseTimeMs(usize), + ResponseTime(Duration), Data(PartialFileData), } @@ -74,7 +76,7 @@ async fn receive_data( let old_number_of_request = shared .number_of_open_requests - .fetch_add(1, atomic::Ordering::SeqCst); + .fetch_add(1, Ordering::SeqCst); let mut measure_ping_time = old_number_of_request == 0; @@ -86,14 +88,11 @@ async fn receive_data( }; if measure_ping_time { - let duration = Instant::now() - request_sent_time; - let duration_ms: u64; - if 0.001 * (duration.as_millis() as f64) > MAXIMUM_ASSUMED_PING_TIME_SECONDS { - duration_ms = (MAXIMUM_ASSUMED_PING_TIME_SECONDS * 1000.0) as u64; - } else { - duration_ms = duration.as_millis() as u64; + let mut duration = Instant::now() - request_sent_time; + if duration > MAXIMUM_ASSUMED_PING_TIME { + duration = MAXIMUM_ASSUMED_PING_TIME; } - let _ = file_data_tx.send(ReceivedData::ResponseTimeMs(duration_ms as usize)); + let _ = file_data_tx.send(ReceivedData::ResponseTime(duration)); measure_ping_time = false; } let data_size = data.len(); @@ -127,7 +126,7 @@ async fn receive_data( shared .number_of_open_requests - .fetch_sub(1, atomic::Ordering::SeqCst); + .fetch_sub(1, Ordering::SeqCst); if result.is_err() { warn!( @@ -149,7 +148,7 @@ struct AudioFileFetch { file_data_tx: mpsc::UnboundedSender, complete_tx: Option>, - network_response_times_ms: Vec, + network_response_times: Vec, } // Might be replaced by enum from std once stable @@ -237,7 +236,7 @@ impl AudioFileFetch { // download data from after the current read position first let mut tail_end = RangeSet::new(); - let read_position = self.shared.read_position.load(atomic::Ordering::Relaxed); + let read_position = self.shared.read_position.load(Ordering::Relaxed); tail_end.add_range(&Range::new( read_position, self.shared.file_size - read_position, @@ -267,26 +266,23 @@ impl AudioFileFetch { fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow { match data { - ReceivedData::ResponseTimeMs(response_time_ms) => { - trace!("Ping time estimated as: {} ms.", response_time_ms); + ReceivedData::ResponseTime(response_time) => { + trace!("Ping time estimated as: {}ms", response_time.as_millis()); - // record the response time - self.network_response_times_ms.push(response_time_ms); - - // prune old response times. Keep at most three. - while self.network_response_times_ms.len() > 3 { - self.network_response_times_ms.remove(0); + // 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); } + // record the response time + self.network_response_times.push(response_time); + // stats::median is experimental. So we calculate the median of up to three ourselves. - let ping_time_ms: usize = match self.network_response_times_ms.len() { - 1 => self.network_response_times_ms[0] as usize, - 2 => { - ((self.network_response_times_ms[0] + self.network_response_times_ms[1]) - / 2) as usize - } + 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_ms.clone(); + let mut times = self.network_response_times.clone(); times.sort_unstable(); times[1] } @@ -296,7 +292,7 @@ impl AudioFileFetch { // store our new estimate for everyone to see self.shared .ping_time_ms - .store(ping_time_ms, atomic::Ordering::Relaxed); + .store(ping_time.as_millis() as usize, Ordering::Relaxed); } ReceivedData::Data(data) => { self.output @@ -390,7 +386,7 @@ pub(super) async fn audio_file_fetch( file_data_tx, complete_tx: Some(complete_tx), - network_response_times_ms: Vec::new(), + network_response_times: Vec::with_capacity(3), }; loop { @@ -408,10 +404,8 @@ pub(super) async fn audio_file_fetch( } if fetch.get_download_strategy() == DownloadStrategy::Streaming() { - let number_of_open_requests = fetch - .shared - .number_of_open_requests - .load(atomic::Ordering::SeqCst); + 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; @@ -424,14 +418,15 @@ pub(super) async fn audio_file_fetch( }; let ping_time_seconds = - 0.001 * fetch.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; + Duration::from_millis(fetch.shared.ping_time_ms.load(Ordering::Relaxed) as u64) + .as_secs_f32(); let download_rate = fetch.session.channel().get_download_rate_estimate(); let desired_pending_bytes = max( (PREFETCH_THRESHOLD_FACTOR * ping_time_seconds - * fetch.shared.stream_data_rate as f64) as usize, - (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f64) + * fetch.shared.stream_data_rate as f32) as usize, + (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f32) as usize, ); diff --git a/audio/src/lib.rs b/audio/src/lib.rs index e43cf728..4b486bbe 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -11,6 +11,6 @@ mod range_set; pub use decrypt::AudioDecrypt; pub use fetch::{AudioFile, StreamLoaderController}; pub use fetch::{ - READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, - READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, + READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, + READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, }; diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 8e9589fc..89d185ab 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -8,25 +8,15 @@ repository = "https://github.com/librespot-org/librespot" edition = "2018" [dependencies] -aes-ctr = "0.6" -base64 = "0.13" form_urlencoded = "1.0" -futures-core = "0.3" futures-util = { version = "0.3.5", default_features = false } -hmac = "0.11" -hyper = { version = "0.14", features = ["server", "http1", "tcp"] } -libmdns = "0.6" log = "0.4" protobuf = "2.14.0" rand = "0.8" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.25" -sha-1 = "0.9" -tokio = { version = "1.0", features = ["macros", "rt", "sync"] } +serde_json = "1.0" +tokio = { version = "1.0", features = ["macros", "sync"] } tokio-stream = "0.1.1" -url = "2.1" - -dns-sd = { version = "0.1.3", optional = true } [dependencies.librespot-core] path = "../core" @@ -40,6 +30,9 @@ version = "0.2.0" path = "../protocol" version = "0.2.0" -[features] -with-dns-sd = ["dns-sd"] +[dependencies.librespot-discovery] +path = "../discovery" +version = "0.2.0" +[features] +with-dns-sd = ["librespot-discovery/with-dns-sd"] diff --git a/connect/src/discovery.rs b/connect/src/discovery.rs index 7d559f0a..8ce3f4f0 100644 --- a/connect/src/discovery.rs +++ b/connect/src/discovery.rs @@ -1,203 +1,19 @@ -use aes_ctr::cipher::generic_array::GenericArray; -use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; -use aes_ctr::Aes128Ctr; -use futures_core::Stream; -use hmac::{Hmac, Mac, NewMac}; -use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Method, Request, Response, StatusCode}; -use serde_json::json; -use sha1::{Digest, Sha1}; -use tokio::sync::{mpsc, oneshot}; - -#[cfg(feature = "with-dns-sd")] -use dns_sd::DNSService; - -use librespot_core::authentication::Credentials; -use librespot_core::config::ConnectConfig; -use librespot_core::diffie_hellman::DhLocalKeys; - -use std::borrow::Cow; -use std::collections::BTreeMap; -use std::convert::Infallible; use std::io; -use std::net::{Ipv4Addr, SocketAddr}; use std::pin::Pin; -use std::sync::Arc; use std::task::{Context, Poll}; -type HmacSha1 = Hmac; +use futures_util::Stream; +use librespot_core::authentication::Credentials; +use librespot_core::config::ConnectConfig; -#[derive(Clone)] -struct Discovery(Arc); -struct DiscoveryInner { - config: ConnectConfig, - device_id: String, - keys: DhLocalKeys, - tx: mpsc::UnboundedSender, -} +pub struct DiscoveryStream(librespot_discovery::Discovery); -impl Discovery { - fn new( - config: ConnectConfig, - device_id: String, - ) -> (Discovery, mpsc::UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded_channel(); +impl Stream for DiscoveryStream { + type Item = Credentials; - let discovery = Discovery(Arc::new(DiscoveryInner { - config, - device_id, - keys: DhLocalKeys::random(&mut rand::thread_rng()), - tx, - })); - - (discovery, rx) + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.0).poll_next(cx) } - - fn handle_get_info(&self, _: BTreeMap, Cow<'_, str>>) -> Response { - let public_key = base64::encode(&self.0.keys.public_key()); - - let result = json!({ - "status": 101, - "statusString": "ERROR-OK", - "spotifyError": 0, - "version": "2.7.1", - "deviceID": (self.0.device_id), - "remoteName": (self.0.config.name), - "activeUser": "", - "publicKey": (public_key), - "deviceType": (self.0.config.device_type.to_string().to_uppercase()), - "libraryVersion": "0.1.0", - "accountReq": "PREMIUM", - "brandDisplayName": "librespot", - "modelDisplayName": "librespot", - "resolverVersion": "0", - "groupStatus": "NONE", - "voiceSupport": "NO", - }); - - let body = result.to_string(); - Response::new(Body::from(body)) - } - - fn handle_add_user( - &self, - params: BTreeMap, Cow<'_, str>>, - ) -> Response { - let username = params.get("userName").unwrap().as_ref(); - let encrypted_blob = params.get("blob").unwrap(); - let client_key = params.get("clientKey").unwrap(); - - let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); - - let shared_key = self - .0 - .keys - .shared_secret(&base64::decode(client_key.as_bytes()).unwrap()); - - let iv = &encrypted_blob[0..16]; - let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; - let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; - - let base_key = Sha1::digest(&shared_key); - let base_key = &base_key[..16]; - - let checksum_key = { - let mut h = HmacSha1::new_from_slice(base_key).expect("HMAC can take key of any size"); - h.update(b"checksum"); - h.finalize().into_bytes() - }; - - let encryption_key = { - let mut h = HmacSha1::new_from_slice(&base_key).expect("HMAC can take key of any size"); - h.update(b"encryption"); - h.finalize().into_bytes() - }; - - let mut h = HmacSha1::new_from_slice(&checksum_key).expect("HMAC can take key of any size"); - h.update(encrypted); - if h.verify(cksum).is_err() { - warn!("Login error for user {:?}: MAC mismatch", username); - let result = json!({ - "status": 102, - "spotifyError": 1, - "statusString": "ERROR-MAC" - }); - - let body = result.to_string(); - return Response::new(Body::from(body)); - } - - let decrypted = { - let mut data = encrypted.to_vec(); - let mut cipher = Aes128Ctr::new( - &GenericArray::from_slice(&encryption_key[0..16]), - &GenericArray::from_slice(iv), - ); - cipher.apply_keystream(&mut data); - String::from_utf8(data).unwrap() - }; - - let credentials = - Credentials::with_blob(username.to_string(), &decrypted, &self.0.device_id); - - self.0.tx.send(credentials).unwrap(); - - let result = json!({ - "status": 101, - "spotifyError": 0, - "statusString": "ERROR-OK" - }); - - let body = result.to_string(); - Response::new(Body::from(body)) - } - - fn not_found(&self) -> Response { - let mut res = Response::default(); - *res.status_mut() = StatusCode::NOT_FOUND; - res - } - - async fn call(self, request: Request) -> hyper::Result> { - let mut params = BTreeMap::new(); - - let (parts, body) = request.into_parts(); - - if let Some(query) = parts.uri.query() { - let query_params = url::form_urlencoded::parse(query.as_bytes()); - params.extend(query_params); - } - - if parts.method != Method::GET { - debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params); - } - - let body = hyper::body::to_bytes(body).await?; - - params.extend(url::form_urlencoded::parse(&body)); - - Ok( - match (parts.method, params.get("action").map(AsRef::as_ref)) { - (Method::GET, Some("getInfo")) => self.handle_get_info(params), - (Method::POST, Some("addUser")) => self.handle_add_user(params), - _ => self.not_found(), - }, - ) - } -} - -#[cfg(feature = "with-dns-sd")] -pub struct DiscoveryStream { - credentials: mpsc::UnboundedReceiver, - _svc: DNSService, - _close_tx: oneshot::Sender, -} - -#[cfg(not(feature = "with-dns-sd"))] -pub struct DiscoveryStream { - credentials: mpsc::UnboundedReceiver, - _svc: libmdns::Service, - _close_tx: oneshot::Sender, } pub fn discovery( @@ -205,59 +21,11 @@ pub fn discovery( device_id: String, port: u16, ) -> io::Result { - let (discovery, creds_rx) = Discovery::new(config.clone(), device_id); - let (close_tx, close_rx) = oneshot::channel(); - - let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port); - - let make_service = make_service_fn(move |_| { - let discovery = discovery.clone(); - async move { Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().call(request))) } - }); - - let server = hyper::Server::bind(&address).serve(make_service); - - let s_port = server.local_addr().port(); - debug!("Zeroconf server listening on 0.0.0.0:{}", s_port); - - tokio::spawn(server.with_graceful_shutdown(async { - close_rx.await.unwrap_err(); - debug!("Shutting down discovery server"); - })); - - #[cfg(feature = "with-dns-sd")] - let svc = DNSService::register( - Some(&*config.name), - "_spotify-connect._tcp", - None, - None, - s_port, - &["VERSION=1.0", "CPath=/"], - ) - .unwrap(); - - #[cfg(not(feature = "with-dns-sd"))] - let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; - - #[cfg(not(feature = "with-dns-sd"))] - let svc = responder.register( - "_spotify-connect._tcp".to_owned(), - config.name, - s_port, - &["VERSION=1.0", "CPath=/"], - ); - - Ok(DiscoveryStream { - credentials: creds_rx, - _svc: svc, - _close_tx: close_tx, - }) -} - -impl Stream for DiscoveryStream { - type Item = Credentials; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.credentials.poll_recv(cx) - } + librespot_discovery::Discovery::builder(device_id) + .device_type(config.device_type) + .port(port) + .name(config.name) + .launch() + .map(DiscoveryStream) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) } diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 600dd033..267bf1b8 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -6,5 +6,9 @@ use librespot_playback as playback; use librespot_protocol as protocol; pub mod context; +#[deprecated( + since = "0.2.1", + note = "Please use the crate `librespot_discovery` instead." +)] pub mod discovery; pub mod spirc; diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index eeb840d2..57dc4cdd 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -3,7 +3,7 @@ use std::pin::Pin; use std::time::{SystemTime, UNIX_EPOCH}; use crate::context::StationContext; -use crate::core::config::{ConnectConfig, VolumeCtrl}; +use crate::core::config::ConnectConfig; use crate::core::mercury::{MercuryError, MercurySender}; use crate::core::session::Session; use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError}; @@ -54,7 +54,6 @@ struct SpircTask { device: DeviceState, state: State, play_request_id: Option, - mixer_started: bool, play_status: SpircPlayStatus, subscription: BoxedStream, @@ -82,13 +81,15 @@ pub enum SpircCommand { } struct SpircTaskConfig { - volume_ctrl: VolumeCtrl, autoplay: bool, } const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; +const VOLUME_STEPS: i64 = 64; +const VOLUME_STEP_SIZE: u16 = 1024; // (u16::MAX + 1) / VOLUME_STEPS + pub struct Spirc { commands: mpsc::UnboundedSender, } @@ -163,10 +164,10 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState { msg.set_typ(protocol::spirc::CapabilityType::kVolumeSteps); { let repeated = msg.mut_intValue(); - if let VolumeCtrl::Fixed = config.volume_ctrl { - repeated.push(0) + if config.has_volume_ctrl { + repeated.push(VOLUME_STEPS) } else { - repeated.push(64) + repeated.push(0) } }; msg @@ -214,36 +215,6 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState { } } -fn calc_logarithmic_volume(volume: u16) -> u16 { - // Volume conversion taken from https://www.dr-lex.be/info-stuff/volumecontrols.html#ideal2 - // Convert the given volume [0..0xffff] to a dB gain - // We assume a dB range of 60dB. - // Use the equation: a * exp(b * x) - // in which a = IDEAL_FACTOR, b = 1/1000 - const IDEAL_FACTOR: f64 = 6.908; - let normalized_volume = volume as f64 / std::u16::MAX as f64; // To get a value between 0 and 1 - - let mut val = std::u16::MAX; - // Prevent val > std::u16::MAX due to rounding errors - if normalized_volume < 0.999 { - let new_volume = (normalized_volume * IDEAL_FACTOR).exp() / 1000.0; - val = (new_volume * std::u16::MAX as f64) as u16; - } - - debug!("input volume:{} to mixer: {}", volume, val); - - // return the scale factor (0..0xffff) (equivalent to a voltage multiplier). - val -} - -fn volume_to_mixer(volume: u16, volume_ctrl: &VolumeCtrl) -> u16 { - match volume_ctrl { - VolumeCtrl::Linear => volume, - VolumeCtrl::Log => calc_logarithmic_volume(volume), - VolumeCtrl::Fixed => volume, - } -} - fn url_encode(bytes: impl AsRef<[u8]>) -> String { form_urlencoded::byte_serialize(bytes.as_ref()).collect() } @@ -280,9 +251,8 @@ impl Spirc { let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); - let volume = config.volume; + let initial_volume = config.initial_volume; let task_config = SpircTaskConfig { - volume_ctrl: config.volume_ctrl.to_owned(), autoplay: config.autoplay, }; @@ -302,7 +272,6 @@ impl Spirc { device, state: initial_state(), play_request_id: None, - mixer_started: false, play_status: SpircPlayStatus::Stopped, subscription, @@ -318,7 +287,12 @@ impl Spirc { context: None, }; - task.set_volume(volume); + if let Some(volume) = initial_volume { + task.set_volume(volume); + } else { + let current_volume = task.mixer.volume(); + task.set_volume(current_volume); + } let spirc = Spirc { commands: cmd_tx }; @@ -437,20 +411,6 @@ impl SpircTask { dur.as_millis() as i64 + 1000 * self.session.time_delta() } - fn ensure_mixer_started(&mut self) { - if !self.mixer_started { - self.mixer.start(); - self.mixer_started = true; - } - } - - fn ensure_mixer_stopped(&mut self) { - if self.mixer_started { - self.mixer.stop(); - self.mixer_started = false; - } - } - fn update_state_position(&mut self, position_ms: u32) { let now = self.now_ms(); self.state.set_position_measured_at(now as u64); @@ -600,7 +560,6 @@ impl SpircTask { _ => { warn!("The player has stopped unexpectedly."); self.state.set_status(PlayStatus::kPlayStatusStop); - self.ensure_mixer_stopped(); self.notify(None, true); self.play_status = SpircPlayStatus::Stopped; } @@ -659,7 +618,6 @@ impl SpircTask { info!("No more tracks left in queue"); self.state.set_status(PlayStatus::kPlayStatusStop); self.player.stop(); - self.mixer.stop(); self.play_status = SpircPlayStatus::Stopped; } @@ -767,7 +725,6 @@ impl SpircTask { self.device.set_is_active(false); self.state.set_status(PlayStatus::kPlayStatusStop); self.player.stop(); - self.ensure_mixer_stopped(); self.play_status = SpircPlayStatus::Stopped; } } @@ -782,7 +739,11 @@ impl SpircTask { position_ms, preloading_of_next_track_triggered, } => { - self.ensure_mixer_started(); + // 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); @@ -792,7 +753,6 @@ impl SpircTask { }; } SpircPlayStatus::LoadingPause { position_ms } => { - self.ensure_mixer_started(); self.player.play(); self.play_status = SpircPlayStatus::LoadingPlay { position_ms }; } @@ -962,7 +922,6 @@ impl SpircTask { self.state.set_playing_track_index(0); self.state.set_status(PlayStatus::kPlayStatusStop); self.player.stop(); - self.ensure_mixer_stopped(); self.play_status = SpircPlayStatus::Stopped; } } @@ -1007,19 +966,13 @@ impl SpircTask { } fn handle_volume_up(&mut self) { - let mut volume: u32 = self.device.get_volume() as u32 + 4096; - if volume > 0xFFFF { - volume = 0xFFFF; - } - self.set_volume(volume as u16); + let volume = (self.device.get_volume() as u16).saturating_add(VOLUME_STEP_SIZE); + self.set_volume(volume); } fn handle_volume_down(&mut self) { - let mut volume: i32 = self.device.get_volume() as i32 - 4096; - if volume < 0 { - volume = 0; - } - self.set_volume(volume as u16); + let volume = (self.device.get_volume() as u16).saturating_sub(VOLUME_STEP_SIZE); + self.set_volume(volume); } fn handle_end_of_track(&mut self) { @@ -1243,7 +1196,6 @@ impl SpircTask { None => { self.state.set_status(PlayStatus::kPlayStatusStop); self.player.stop(); - self.ensure_mixer_stopped(); self.play_status = SpircPlayStatus::Stopped; } } @@ -1273,8 +1225,7 @@ impl SpircTask { fn set_volume(&mut self, volume: u16) { self.device.set_volume(volume as u32); - self.mixer - .set_volume(volume_to_mixer(volume, &self.config.volume_ctrl)); + self.mixer.set_volume(volume); if let Some(cache) = self.session.cache() { cache.save_volume(volume) } diff --git a/contrib/librespot.service b/contrib/librespot.service index bd381df2..76037c8c 100644 --- a/contrib/librespot.service +++ b/contrib/librespot.service @@ -1,5 +1,7 @@ [Unit] -Description=Librespot +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 @@ -8,8 +10,7 @@ User=nobody Group=audio Restart=always RestartSec=10 -ExecStart=/usr/bin/librespot -n "%p on %H" +ExecStart=/usr/bin/librespot --name "%p@%H" [Install] WantedBy=multi-user.target - diff --git a/contrib/librespot.user.service b/contrib/librespot.user.service new file mode 100644 index 00000000..a676dde0 --- /dev/null +++ b/contrib/librespot.user.service @@ -0,0 +1,12 @@ +[Unit] +Description=Librespot (an open source Spotify client) +Documentation=https://github.com/librespot-org/librespot +Documentation=https://github.com/librespot-org/librespot/wiki/Options + +[Service] +Restart=always +RestartSec=10 +ExecStart=/usr/bin/librespot --name "%u@%H" + +[Install] +WantedBy=default.target diff --git a/core/Cargo.toml b/core/Cargo.toml index 7eb4051c..3c239034 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -26,7 +26,9 @@ http = "0.2" hyper = { version = "0.14", features = ["client", "tcp", "http1"] } hyper-proxy = { version = "0.9.1", default-features = false } log = "0.4" +num = "0.4" num-bigint = { version = "0.4", features = ["rand"] } +num-derive = "0.3" num-integer = "0.1" num-traits = "0.2" once_cell = "1.5.2" diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 975e0e18..623c7cb3 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,132 +1,141 @@ -use std::error::Error; - -use hyper::client::HttpConnector; -use hyper::{Body, Client, Request}; -use hyper_proxy::{Intercept, Proxy, ProxyConnector}; +use hyper::{Body, Request}; use serde::Deserialize; -use url::Url; - -const APRESOLVE_ENDPOINT: &str = - "http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient"; - -// These addresses probably do some geo-location based traffic management or at least DNS-based -// load balancing. They are known to fail when the normal resolvers are up, so that's why they -// should only be used as fallback. -const AP_FALLBACK: &str = "ap.spotify.com"; -const DEALER_FALLBACK: &str = "dealer.spotify.com"; -const SPCLIENT_FALLBACK: &str = "spclient.wg.spotify.com"; - -const FALLBACK_PORT: u16 = 443; +use std::error::Error; +use std::sync::atomic::{AtomicUsize, Ordering}; pub type SocketAddress = (String, u16); -#[derive(Clone, Debug, Default, Deserialize)] +#[derive(Default)] +struct AccessPoints { + accesspoint: Vec, + dealer: Vec, + spclient: Vec, +} + +#[derive(Deserialize)] struct ApResolveData { accesspoint: Vec, dealer: Vec, spclient: Vec, } -#[derive(Clone, Debug, Deserialize)] -pub struct AccessPoints { - pub accesspoint: SocketAddress, - pub dealer: SocketAddress, - pub spclient: SocketAddress, -} - -fn select_ap(data: Vec, fallback: &str, ap_port: Option) -> SocketAddress { - let port = ap_port.unwrap_or(FALLBACK_PORT); - - let mut aps = data.into_iter().filter_map(|ap| { - let mut split = ap.rsplitn(2, ':'); - let port = split - .next() - .expect("rsplitn should not return empty iterator"); - let host = split.next()?.to_owned(); - let port: u16 = port.parse().ok()?; - Some((host, port)) - }); - - let ap = if ap_port.is_some() { - aps.find(|(_, p)| *p == port) - } else { - aps.next() - }; - - ap.unwrap_or_else(|| (String::from(fallback), port)) -} - -async fn try_apresolve(proxy: Option<&Url>) -> Result> { - let req = Request::builder() - .method("GET") - .uri(APRESOLVE_ENDPOINT) - .body(Body::empty()) - .unwrap(); - - let response = if let Some(url) = proxy { - // Panic safety: all URLs are valid URIs - let uri = url.to_string().parse().unwrap(); - 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? - } else { - Client::new().request(req).await? - }; - - let body = hyper::body::to_bytes(response.into_body()).await?; - let data: ApResolveData = serde_json::from_slice(body.as_ref())?; - - Ok(data) -} - -pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> AccessPoints { - let data = try_apresolve(proxy).await.unwrap_or_else(|e| { - warn!("Failed to resolve access points: {}, using fallbacks.", e); - ApResolveData::default() - }); - - let accesspoint = select_ap(data.accesspoint, AP_FALLBACK, ap_port); - let dealer = select_ap(data.dealer, DEALER_FALLBACK, ap_port); - let spclient = select_ap(data.spclient, SPCLIENT_FALLBACK, ap_port); - - AccessPoints { - accesspoint, - dealer, - spclient, +// These addresses probably do some geo-location based traffic management or at least DNS-based +// load balancing. They are known to fail when the normal resolvers are up, so that's why they +// should only be used as fallback. +impl Default for ApResolveData { + fn default() -> Self { + Self { + accesspoint: vec![String::from("ap.spotify.com:443")], + dealer: vec![String::from("dealer.spotify.com:443")], + spclient: vec![String::from("spclient.wg.spotify.com:443")], + } } } -#[cfg(test)] -mod test { - use std::net::ToSocketAddrs; - - use super::apresolve; - - #[tokio::test] - async fn test_apresolve() { - let aps = apresolve(None, None).await; - - // Assert that the result contains a valid host and port - aps.accesspoint.to_socket_addrs().unwrap().next().unwrap(); - aps.dealer.to_socket_addrs().unwrap().next().unwrap(); - aps.spclient.to_socket_addrs().unwrap().next().unwrap(); - } - - #[tokio::test] - async fn test_apresolve_port_443() { - let aps = apresolve(None, Some(443)).await; - - let port = aps - .accesspoint - .to_socket_addrs() - .unwrap() - .next() - .unwrap() - .port(); - assert_eq!(port, 443); +component! { + ApResolver : ApResolverInner { + data: AccessPoints = AccessPoints::default(), + spinlock: AtomicUsize = AtomicUsize::new(0), + } +} + +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 { + if self.session().config().proxy.is_some() || self.session().config().ap_port.is_some() { + Some(self.session().config().ap_port.unwrap_or(443)) + } else { + None + } + } + + fn process_data(&self, data: Vec) -> Vec { + data.into_iter() + .filter_map(|ap| { + let mut split = ap.rsplitn(2, ':'); + let port = split + .next() + .expect("rsplitn should not return empty iterator"); + let host = split.next()?.to_owned(); + let port: u16 = port.parse().ok()?; + if let Some(p) = self.port_config() { + if p != port { + return None; + } + } + Some((host, port)) + }) + .collect() + } + + 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(); + + let body = self.session().http_client().request_body(req).await?; + let data: ApResolveData = serde_json::from_slice(body.as_ref())?; + + Ok(data) + } + + async fn apresolve(&self) { + let result = self.try_apresolve().await; + + self.lock(|inner| { + let data = match result { + Ok(data) => data, + Err(e) => { + warn!("Failed to resolve access points, using fallbacks: {}", e); + ApResolveData::default() + } + }; + + inner.data.accesspoint = self.process_data(data.accesspoint); + inner.data.dealer = self.process_data(data.dealer); + inner.data.spclient = self.process_data(data.spclient); + }) + } + + fn is_empty(&self) -> bool { + self.lock(|inner| { + inner.data.accesspoint.is_empty() + || inner.data.dealer.is_empty() + || inner.data.spclient.is_empty() + }) + } + + pub async fn resolve(&self, endpoint: &str) -> SocketAddress { + // Use a spinlock to make this function atomic. Otherwise, various race conditions may + // occur, e.g. when the session is created, multiple components are launched almost in + // parallel and they will all call this function, while resolving is still in progress. + self.lock(|inner| { + while inner.spinlock.load(Ordering::SeqCst) != 0 { + #[allow(deprecated)] + std::sync::atomic::spin_loop_hint() + } + inner.spinlock.store(1, Ordering::SeqCst); + }); + + if self.is_empty() { + self.apresolve().await; + } + + self.lock(|inner| { + let access_point = match endpoint { + // take the first position instead of the last with `pop`, because Spotify returns + // access points with ports 4070, 443 and 80 in order of preference from highest + // to lowest. + "accesspoint" => inner.data.accesspoint.remove(0), + "dealer" => inner.data.dealer.remove(0), + "spclient" => inner.data.spclient.remove(0), + _ => unimplemented!(), + }; + inner.spinlock.store(0, Ordering::SeqCst); + access_point + }) } } diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index 3bce1c73..f42c6502 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::io::Write; use tokio::sync::oneshot; +use crate::packet::PacketType; use crate::spotify_id::{FileId, SpotifyId}; use crate::util::SeqGenerator; @@ -21,19 +22,19 @@ component! { } impl AudioKeyManager { - pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { let seq = BigEndian::read_u32(data.split_to(4).as_ref()); let sender = self.lock(|inner| inner.pending.remove(&seq)); if let Some(sender) = sender { match cmd { - 0xd => { + PacketType::AesKey => { let mut key = [0u8; 16]; key.copy_from_slice(data.as_ref()); let _ = sender.send(Ok(AudioKey(key))); } - 0xe => { + PacketType::AesKeyError => { warn!( "error audio key {:x} {:x}", data.as_ref()[0], @@ -61,11 +62,11 @@ impl AudioKeyManager { fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) { let mut data: Vec = Vec::new(); - data.write(&file.0).unwrap(); - data.write(&track.to_raw()).unwrap(); + data.write_all(&file.0).unwrap(); + data.write_all(&track.to_raw()).unwrap(); data.write_u32::(seq).unwrap(); data.write_u16::(0x0000).unwrap(); - self.session().send_packet(0xc, data) + self.session().send_packet(PacketType::RequestKey, data) } } diff --git a/core/src/channel.rs b/core/src/channel.rs index 4a78a4aa..31c01a40 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -8,8 +8,10 @@ use bytes::Bytes; use futures_core::Stream; use futures_util::lock::BiLock; use futures_util::{ready, StreamExt}; +use num_traits::FromPrimitive; use tokio::sync::mpsc; +use crate::packet::PacketType; use crate::util::SeqGenerator; component! { @@ -23,6 +25,8 @@ component! { } } +const ONE_SECOND_IN_MS: usize = 1000; + #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] pub struct ChannelError; @@ -66,7 +70,7 @@ impl ChannelManager { (seq, channel) } - pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { use std::collections::hash_map::Entry; let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref()); @@ -74,8 +78,11 @@ impl ChannelManager { self.lock(|inner| { let current_time = Instant::now(); if let Some(download_measurement_start) = inner.download_measurement_start { - if (current_time - download_measurement_start).as_millis() > 1000 { - inner.download_rate_estimate = 1000 * inner.download_measurement_bytes + if (current_time - download_measurement_start).as_millis() + > ONE_SECOND_IN_MS as u128 + { + inner.download_rate_estimate = ONE_SECOND_IN_MS + * inner.download_measurement_bytes / (current_time - download_measurement_start).as_millis() as usize; inner.download_measurement_start = Some(current_time); inner.download_measurement_bytes = 0; @@ -87,7 +94,7 @@ impl ChannelManager { inner.download_measurement_bytes += data.len(); if let Entry::Occupied(entry) = inner.channels.entry(id) { - let _ = entry.get().send((cmd, data)); + let _ = entry.get().send((cmd as u8, data)); } }); } @@ -109,7 +116,8 @@ impl Channel { fn recv_packet(&mut self, cx: &mut Context<'_>) -> Poll> { let (cmd, packet) = ready!(self.receiver.poll_recv(cx)).ok_or(ChannelError)?; - if cmd == 0xa { + let packet_type = FromPrimitive::from_u8(cmd); + if let Some(PacketType::ChannelError) = packet_type { let code = BigEndian::read_u16(&packet.as_ref()[..2]); error!("channel error: {} {}", packet.len(), code); diff --git a/core/src/config.rs b/core/src/config.rs index 9c70c25b..0e3eaf4a 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -71,30 +71,43 @@ impl FromStr for DeviceType { } } +impl From<&DeviceType> for &str { + fn from(d: &DeviceType) -> &'static str { + use self::DeviceType::*; + match d { + Unknown => "Unknown", + Computer => "Computer", + Tablet => "Tablet", + Smartphone => "Smartphone", + Speaker => "Speaker", + Tv => "TV", + Avr => "AVR", + Stb => "STB", + AudioDongle => "AudioDongle", + GameConsole => "GameConsole", + CastAudio => "CastAudio", + CastVideo => "CastVideo", + Automobile => "Automobile", + Smartwatch => "Smartwatch", + Chromebook => "Chromebook", + UnknownSpotify => "UnknownSpotify", + CarThing => "CarThing", + Observer => "Observer", + HomeThing => "HomeThing", + } + } +} + +impl From for &str { + fn from(d: DeviceType) -> &'static str { + (&d).into() + } +} + impl fmt::Display for DeviceType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use self::DeviceType::*; - match *self { - Unknown => f.write_str("Unknown"), - Computer => f.write_str("Computer"), - Tablet => f.write_str("Tablet"), - Smartphone => f.write_str("Smartphone"), - Speaker => f.write_str("Speaker"), - Tv => f.write_str("TV"), - Avr => f.write_str("AVR"), - Stb => f.write_str("STB"), - AudioDongle => f.write_str("AudioDongle"), - GameConsole => f.write_str("GameConsole"), - CastAudio => f.write_str("CastAudio"), - CastVideo => f.write_str("CastVideo"), - Automobile => f.write_str("Automobile"), - Smartwatch => f.write_str("Smartwatch"), - Chromebook => f.write_str("Chromebook"), - UnknownSpotify => f.write_str("UnknownSpotify"), - CarThing => f.write_str("CarThing"), - Observer => f.write_str("Observer"), - HomeThing => f.write_str("HomeThing"), - } + let str: &str = self.into(); + f.write_str(str) } } @@ -108,33 +121,7 @@ impl Default for DeviceType { pub struct ConnectConfig { pub name: String, pub device_type: DeviceType, - pub volume: u16, - pub volume_ctrl: VolumeCtrl, + pub initial_volume: Option, + pub has_volume_ctrl: bool, pub autoplay: bool, } - -#[derive(Clone, Debug)] -pub enum VolumeCtrl { - Linear, - Log, - Fixed, -} - -impl FromStr for VolumeCtrl { - type Err = (); - fn from_str(s: &str) -> Result { - use self::VolumeCtrl::*; - match s.to_lowercase().as_ref() { - "linear" => Ok(Linear), - "log" => Ok(Log), - "fixed" => Ok(Fixed), - _ => Err(()), - } - } -} - -impl Default for VolumeCtrl { - fn default() -> VolumeCtrl { - VolumeCtrl::Log - } -} diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index bacdc653..472109e6 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -7,6 +7,7 @@ pub use self::handshake::handshake; use std::io::{self, ErrorKind}; use futures_util::{SinkExt, StreamExt}; +use num_traits::FromPrimitive; use protobuf::{self, Message, ProtobufError}; use thiserror::Error; use tokio::net::TcpStream; @@ -14,6 +15,7 @@ use tokio_util::codec::Framed; use url::Url; use crate::authentication::Credentials; +use crate::packet::PacketType; use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; use crate::version; @@ -95,13 +97,14 @@ pub async fn authenticate( .set_device_id(device_id.to_string()); packet.set_version_string(version::VERSION_STRING.to_string()); - let cmd = 0xab; + let cmd = PacketType::Login; let data = packet.write_to_bytes().unwrap(); - transport.send((cmd, data)).await?; + transport.send((cmd as u8, data)).await?; let (cmd, data) = transport.next().await.expect("EOF")?; - match cmd { - 0xac => { + let packet_type = FromPrimitive::from_u8(cmd); + match packet_type { + Some(PacketType::APWelcome) => { let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?; let reusable_credentials = Credentials { @@ -112,7 +115,7 @@ pub async fn authenticate( Ok(reusable_credentials) } - 0xad => { + Some(PacketType::AuthFailure) => { let error_data = APLoginFailed::parse_from_bytes(data.as_ref())?; Err(error_data.into()) } diff --git a/core/src/http_client.rs b/core/src/http_client.rs new file mode 100644 index 00000000..5f8ef780 --- /dev/null +++ b/core/src/http_client.rs @@ -0,0 +1,34 @@ +use hyper::client::HttpConnector; +use hyper::{Body, Client, Request, Response}; +use hyper_proxy::{Intercept, Proxy, ProxyConnector}; +use url::Url; + +pub struct HttpClient { + proxy: Option, +} + +impl HttpClient { + pub fn new(proxy: Option<&Url>) -> Self { + Self { + proxy: proxy.cloned(), + } + } + + 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(); + 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 + } else { + Client::new().request(req).await + } + } + + 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/keymaster.rs b/core/src/keymaster.rs deleted file mode 100644 index 8c3c00a2..00000000 --- a/core/src/keymaster.rs +++ /dev/null @@ -1,26 +0,0 @@ -use serde::Deserialize; - -use crate::{mercury::MercuryError, session::Session}; - -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Token { - pub access_token: String, - pub expires_in: u32, - pub token_type: String, - pub scope: Vec, -} - -pub async fn get_token( - session: &Session, - client_id: &str, - scopes: &str, -) -> Result { - let url = format!( - "hm://keymaster/token/authenticated?client_id={}&scope={}", - client_id, scopes - ); - let response = session.mercury().get(url).await?; - let data = response.payload.first().expect("Empty payload"); - serde_json::from_slice(data.as_ref()).map_err(|_| MercuryError) -} diff --git a/core/src/lib.rs b/core/src/lib.rs index f26caf3d..9c92c235 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,7 +1,6 @@ -#![allow(clippy::unused_io_amount)] - #[macro_use] extern crate log; +extern crate num_derive; use librespot_protocol as protocol; @@ -19,12 +18,15 @@ mod connection; mod dealer; #[doc(hidden)] pub mod diffie_hellman; -pub mod keymaster; +mod http_client; pub mod mercury; +pub mod packet; mod proxytunnel; pub mod session; mod socket; +mod spclient; pub mod spotify_id; +mod token; #[doc(hidden)] pub mod util; pub mod version; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 57650087..6cf3519e 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -11,6 +11,7 @@ use futures_util::FutureExt; use protobuf::Message; use tokio::sync::{mpsc, oneshot}; +use crate::packet::PacketType; use crate::protocol; use crate::util::SeqGenerator; @@ -143,7 +144,7 @@ impl MercuryManager { } } - pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { + 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(); @@ -154,14 +155,17 @@ impl MercuryManager { let mut pending = match pending { Some(pending) => pending, - None if cmd == 0xb5 => MercuryPending { - parts: Vec::new(), - partial: None, - callback: None, - }, None => { - warn!("Ignore seq {:?} cmd {:x}", seq, cmd); - return; + if let PacketType::MercuryEvent = cmd { + MercuryPending { + parts: Vec::new(), + partial: None, + callback: None, + } + } else { + warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8); + return; + } } }; @@ -191,7 +195,7 @@ impl MercuryManager { data.split_to(size).as_ref().to_owned() } - fn complete_request(&self, cmd: u8, mut pending: MercuryPending) { + fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) { let header_data = pending.parts.remove(0); let header = protocol::mercury::Header::parse_from_bytes(&header_data).unwrap(); @@ -208,7 +212,7 @@ impl MercuryManager { if let Some(cb) = pending.callback { let _ = cb.send(Err(MercuryError)); } - } else if cmd == 0xb5 { + } else if let PacketType::MercuryEvent = cmd { self.lock(|inner| { let mut found = false; diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 402a954c..1d6b5b15 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -2,6 +2,7 @@ use byteorder::{BigEndian, WriteBytesExt}; use protobuf::Message; use std::io::Write; +use crate::packet::PacketType; use crate::protocol; #[derive(Debug, PartialEq, Eq)] @@ -43,11 +44,12 @@ impl ToString for MercuryMethod { } impl MercuryMethod { - pub fn command(&self) -> u8 { + pub fn command(&self) -> PacketType { + use PacketType::*; match *self { - MercuryMethod::Get | MercuryMethod::Send => 0xb2, - MercuryMethod::Sub => 0xb3, - MercuryMethod::Unsub => 0xb4, + MercuryMethod::Get | MercuryMethod::Send => MercuryReq, + MercuryMethod::Sub => MercurySub, + MercuryMethod::Unsub => MercuryUnsub, } } } @@ -77,7 +79,7 @@ impl MercuryRequest { for p in &self.payload { packet.write_u16::(p.len() as u16).unwrap(); - packet.write(p).unwrap(); + packet.write_all(p).unwrap(); } packet diff --git a/core/src/packet.rs b/core/src/packet.rs new file mode 100644 index 00000000..de780f13 --- /dev/null +++ b/core/src/packet.rs @@ -0,0 +1,41 @@ +// Ported from librespot-java. Relicensed under MIT with permission. + +use num_derive::{FromPrimitive, ToPrimitive}; + +#[derive(Debug, FromPrimitive, ToPrimitive)] +pub enum PacketType { + SecretBlock = 0x02, + Ping = 0x04, + StreamChunk = 0x08, + StreamChunkRes = 0x09, + ChannelError = 0x0a, + ChannelAbort = 0x0b, + RequestKey = 0x0c, + AesKey = 0x0d, + AesKeyError = 0x0e, + Image = 0x19, + CountryCode = 0x1b, + Pong = 0x49, + PongAck = 0x4a, + Pause = 0x4b, + ProductInfo = 0x50, + LegacyWelcome = 0x69, + LicenseVersion = 0x76, + Login = 0xab, + APWelcome = 0xac, + AuthFailure = 0xad, + MercuryReq = 0xb2, + MercurySub = 0xb3, + MercuryUnsub = 0xb4, + MercuryEvent = 0xb5, + TrackEndedTime = 0x82, + UnknownDataAllZeros = 0x1f, + PreferredLocale = 0x74, + Unknown0x0f = 0x0f, + Unknown0x10 = 0x10, + Unknown0x4f = 0x4f, + + // TODO - occurs when subscribing with an empty URI. Maybe a MercuryError? + // Payload: b"\0\x08\0\0\0\0\0\0\0\0\x01\0\x01\0\x03 \xb0\x06" + Unknown0xb6 = 0xb6, +} diff --git a/core/src/session.rs b/core/src/session.rs index 17452b20..81975a80 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -11,19 +11,23 @@ use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use futures_core::TryStream; use futures_util::{future, ready, StreamExt, TryStreamExt}; +use num_traits::FromPrimitive; use once_cell::sync::OnceCell; use thiserror::Error; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; -use crate::apresolve::apresolve; +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::token::TokenProvider; #[derive(Debug, Error)] pub enum SessionError { @@ -44,11 +48,14 @@ struct SessionInternal { config: SessionConfig, data: RwLock, + http_client: HttpClient, tx_connection: mpsc::UnboundedSender<(u8, Vec)>, + apresolver: OnceCell, audio_key: OnceCell, channel: OnceCell, mercury: OnceCell, + token_provider: OnceCell, cache: Option>, handle: tokio::runtime::Handle, @@ -67,40 +74,7 @@ impl Session { credentials: Credentials, cache: Option, ) -> Result { - let ap = apresolve(config.proxy.as_ref(), config.ap_port) - .await - .accesspoint; - - info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); - let mut conn = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?; - - let reusable_credentials = - connection::authenticate(&mut conn, credentials, &config.device_id).await?; - info!("Authenticated as \"{}\" !", reusable_credentials.username); - if let Some(cache) = &cache { - cache.save_credentials(&reusable_credentials); - } - - let session = Session::create( - conn, - config, - cache, - reusable_credentials.username, - tokio::runtime::Handle::current(), - ); - - Ok(session) - } - - fn create( - transport: connection::Transport, - config: SessionConfig, - cache: Option, - username: String, - handle: tokio::runtime::Handle, - ) -> Session { - let (sink, stream) = transport.split(); - + 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); @@ -110,19 +84,37 @@ impl Session { config, data: RwLock::new(SessionData { country: String::new(), - canonical_username: username, + canonical_username: String::new(), invalid: false, time_delta: 0, }), + http_client, tx_connection: sender_tx, cache: cache.map(Arc::new), + apresolver: OnceCell::new(), audio_key: OnceCell::new(), channel: OnceCell::new(), mercury: OnceCell::new(), - handle, + token_provider: OnceCell::new(), + handle: tokio::runtime::Handle::current(), session_id, })); + let ap = session.apresolver().resolve("accesspoint").await; + info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); + let mut transport = + connection::connect(&ap.0, ap.1, session.config().proxy.as_ref()).await?; + + let reusable_credentials = + 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(); + if let Some(cache) = session.cache() { + cache.save_credentials(&reusable_credentials); + } + + let (sink, stream) = transport.split(); let sender_task = UnboundedReceiverStream::new(sender_rx) .map(Ok) .forward(sink); @@ -136,7 +128,13 @@ impl Session { } }); - session + Ok(session) + } + + pub fn apresolver(&self) -> &ApResolver { + self.0 + .apresolver + .get_or_init(|| ApResolver::new(self.weak())) } pub fn audio_key(&self) -> &AudioKeyManager { @@ -151,12 +149,22 @@ impl Session { .get_or_init(|| ChannelManager::new(self.weak())) } + pub fn http_client(&self) -> &HttpClient { + &self.0.http_client + } + pub fn mercury(&self) -> &MercuryManager { self.0 .mercury .get_or_init(|| MercuryManager::new(self.weak())) } + pub fn token_provider(&self) -> &TokenProvider { + self.0 + .token_provider + .get_or_init(|| TokenProvider::new(self.weak())) + } + pub fn time_delta(&self) -> i64 { self.0.data.read().unwrap().time_delta } @@ -178,10 +186,11 @@ impl Session { ); } - #[allow(clippy::match_same_arms)] fn dispatch(&self, cmd: u8, data: Bytes) { - match cmd { - 0x4 => { + use PacketType::*; + let packet_type = FromPrimitive::from_u8(cmd); + match packet_type { + Some(Ping) => { let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64; let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(dur) => dur, @@ -192,31 +201,47 @@ impl Session { self.0.data.write().unwrap().time_delta = server_timestamp - timestamp; self.debug_info(); - self.send_packet(0x49, vec![0, 0, 0, 0]); + self.send_packet(Pong, vec![0, 0, 0, 0]); } - 0x4a => (), - 0x1b => { + Some(CountryCode) => { let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); info!("Country: {:?}", country); self.0.data.write().unwrap().country = country; } - - 0x9 | 0xa => self.channel().dispatch(cmd, data), - 0xd | 0xe => self.audio_key().dispatch(cmd, data), - 0xb2..=0xb6 => self.mercury().dispatch(cmd, data), - _ => (), + Some(StreamChunkRes) | Some(ChannelError) => { + self.channel().dispatch(packet_type.unwrap(), data); + } + Some(AesKey) | Some(AesKeyError) => { + self.audio_key().dispatch(packet_type.unwrap(), data); + } + Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => { + self.mercury().dispatch(packet_type.unwrap(), data); + } + Some(PongAck) + | Some(SecretBlock) + | Some(LegacyWelcome) + | Some(UnknownDataAllZeros) + | Some(ProductInfo) + | Some(LicenseVersion) => {} + _ => { + if let Some(packet_type) = PacketType::from_u8(cmd) { + trace!("Ignoring {:?} packet with data {:?}", packet_type, data); + } else { + trace!("Ignoring unknown packet {:x}", cmd); + } + } } } - pub fn send_packet(&self, cmd: u8, data: Vec) { - self.0.tx_connection.send((cmd, data)).unwrap(); + pub fn send_packet(&self, cmd: PacketType, data: Vec) { + self.0.tx_connection.send((cmd as u8, data)).unwrap(); } pub fn cache(&self) -> Option<&Arc> { self.0.cache.as_ref() } - fn config(&self) -> &SessionConfig { + pub fn config(&self) -> &SessionConfig { &self.0.config } diff --git a/core/src/spclient.rs b/core/src/spclient.rs new file mode 100644 index 00000000..eb7b3f0f --- /dev/null +++ b/core/src/spclient.rs @@ -0,0 +1 @@ +// https://github.com/librespot-org/librespot-java/blob/27783e06f456f95228c5ac37acf2bff8c1a8a0c4/lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 3372572a..e6e2bae0 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -116,22 +116,25 @@ impl SpotifyId { /// /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids pub fn from_uri(src: &str) -> Result { - // We expect the ID to be the last colon-delimited item in the URI. - let b = src.as_bytes(); - let id_i = b.len() - SpotifyId::SIZE_BASE62; - if b[id_i - 1] != b':' { + let src = src.strip_prefix("spotify:").ok_or(SpotifyIdError)?; + + if src.len() <= SpotifyId::SIZE_BASE62 { return Err(SpotifyIdError); } - let mut id = SpotifyId::from_base62(&src[id_i..])?; + let colon_index = src.len() - SpotifyId::SIZE_BASE62 - 1; - // Slice offset by 8 as we are skipping the "spotify:" prefix. - id.audio_type = src[8..id_i - 1].into(); + if src.as_bytes()[colon_index] != b':' { + return Err(SpotifyIdError); + } + + let mut id = SpotifyId::from_base62(&src[colon_index + 1..])?; + id.audio_type = src[..colon_index].into(); Ok(id) } - /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE62` (22) + /// 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]) @@ -305,7 +308,7 @@ mod tests { }, ]; - static CONV_INVALID: [ConversionCase; 2] = [ + static CONV_INVALID: [ConversionCase; 3] = [ ConversionCase { id: 0, kind: SpotifyAudioType::NonPlayable, @@ -330,6 +333,18 @@ mod tests { 154, 27, 28, 251, ], }, + ConversionCase { + id: 0, + kind: SpotifyAudioType::NonPlayable, + // Uri too short + uri: "spotify:azb:aRS48xBl0tH", + base16: "--------------------", + base62: "....................", + raw: &[ + // Invalid length. + 154, 27, 28, 251, + ], + }, ]; #[test] diff --git a/core/src/token.rs b/core/src/token.rs new file mode 100644 index 00000000..824fcc3b --- /dev/null +++ b/core/src/token.rs @@ -0,0 +1,131 @@ +// Ported from librespot-java. Relicensed under MIT with permission. + +// Known scopes: +// ugc-image-upload, playlist-read-collaborative, playlist-modify-private, +// playlist-modify-public, playlist-read-private, user-read-playback-position, +// user-read-recently-played, user-top-read, user-modify-playback-state, +// user-read-currently-playing, user-read-playback-state, user-read-private, user-read-email, +// user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming, +// app-remote-control + +use crate::mercury::MercuryError; + +use serde::Deserialize; + +use std::error::Error; +use std::time::{Duration, Instant}; + +component! { + TokenProvider : TokenProviderInner { + tokens: Vec = vec![], + } +} + +#[derive(Clone, Debug)] +pub struct Token { + access_token: String, + expires_in: Duration, + token_type: String, + scopes: Vec, + timestamp: Instant, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenData { + access_token: String, + expires_in: u64, + token_type: String, + scope: Vec, +} + +impl TokenProvider { + const KEYMASTER_CLIENT_ID: &'static str = "65b708073fc0480ea92a077233ca87bd"; + + fn find_token(&self, scopes: Vec<&str>) -> Option { + self.lock(|inner| { + for i in 0..inner.tokens.len() { + if inner.tokens[i].in_scopes(scopes.clone()) { + return Some(i); + } + } + None + }) + } + + // scopes must be comma-separated + pub async fn get_token(&self, scopes: &str) -> Result { + if scopes.is_empty() { + return Err(MercuryError); + } + + 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() { + self.lock(|inner| inner.tokens.remove(index)); + } else { + return Ok(cached_token); + } + } + + trace!( + "Requested token in scopes {:?} unavailable or expired, requesting new token.", + scopes + ); + + let query_uri = format!( + "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}", + scopes, + Self::KEYMASTER_CLIENT_ID, + self.session().device_id() + ); + 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)?; + trace!("Got token: {:?}", token); + self.lock(|inner| inner.tokens.push(token.clone())); + Ok(token) + } +} + +impl Token { + const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10); + + pub fn new(body: String) -> Result> { + let data: TokenData = serde_json::from_slice(body.as_ref())?; + Ok(Self { + access_token: data.access_token, + expires_in: Duration::from_secs(data.expires_in), + token_type: data.token_type, + scopes: data.scope, + timestamp: Instant::now(), + }) + } + + pub fn is_expired(&self) -> bool { + self.timestamp + (self.expires_in - Self::EXPIRY_THRESHOLD) < Instant::now() + } + + pub fn in_scope(&self, scope: &str) -> bool { + for s in &self.scopes { + if *s == scope { + return true; + } + } + false + } + + pub fn in_scopes(&self, scopes: Vec<&str>) -> bool { + for s in scopes { + if !self.in_scope(s) { + return false; + } + } + true + } +} diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml new file mode 100644 index 00000000..9ea9df48 --- /dev/null +++ b/discovery/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "librespot-discovery" +version = "0.2.0" +authors = ["Paul Lietar "] +description = "The discovery logic for librespot" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2018" + +[dependencies] +aes-ctr = "0.6" +base64 = "0.13" +cfg-if = "1.0" +form_urlencoded = "1.0" +futures-core = "0.3" +hmac = "0.11" +hyper = { version = "0.14", features = ["server", "http1", "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"] } + +dns-sd = { version = "0.1.3", optional = true } + +[dependencies.librespot-core] +path = "../core" +default_features = false +version = "0.2.0" + +[dev-dependencies] +futures = "0.3" +hex = "0.4" +simple_logger = "1.11" +tokio = { version = "1.0", features = ["macros", "rt"] } + +[features] +with-dns-sd = ["dns-sd"] diff --git a/discovery/examples/discovery.rs b/discovery/examples/discovery.rs new file mode 100644 index 00000000..cd913fd2 --- /dev/null +++ b/discovery/examples/discovery.rs @@ -0,0 +1,25 @@ +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())); + + let mut server = librespot_discovery::Discovery::builder(device_id) + .name(name) + .device_type(DeviceType::Computer) + .launch() + .unwrap(); + + while let Some(x) = server.next().await { + println!("Received {:?}", x); + } +} diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs new file mode 100644 index 00000000..b1249a0d --- /dev/null +++ b/discovery/src/lib.rs @@ -0,0 +1,150 @@ +//! Advertises this device to Spotify clients in the local network. +//! +//! This device will show up in the list of "available devices". +//! Once it is selected from the list, [`Credentials`] are received. +//! Those can be used to establish a new Session with [`librespot_core`]. +//! +//! 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; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use cfg_if::cfg_if; +use futures_core::Stream; +use librespot_core as core; +use thiserror::Error; + +use self::server::DiscoveryServer; + +/// Credentials to be used in [`librespot`](`librespot_core`). +pub use crate::core::authentication::Credentials; + +/// Determining the icon in the list of available devices. +pub use crate::core::config::DeviceType; + +/// Makes this device visible to Spotify clients in the local network. +/// +/// `Discovery` implements the [`Stream`] trait. Every time this device +/// is selected in the list of available devices, it yields [`Credentials`]. +pub struct Discovery { + server: DiscoveryServer, + + #[cfg(not(feature = "with-dns-sd"))] + _svc: libmdns::Service, + #[cfg(feature = "with-dns-sd")] + _svc: dns_sd::DNSService, +} + +/// A builder for [`Discovery`]. +pub struct Builder { + server_config: server::Config, + port: u16, +} + +/// Errors that can occur while setting up a [`Discovery`] instance. +#[derive(Debug, Error)] +pub enum Error { + /// 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("Setting up the http server failed: {0}")] + HttpServerError(#[from] hyper::Error), +} + +impl Builder { + /// Starts a new builder using the provided device id. + pub fn new(device_id: impl Into) -> Self { + Self { + server_config: server::Config { + name: "Librespot".into(), + device_type: DeviceType::default(), + device_id: device_id.into(), + }, + port: 0, + } + } + + /// Sets the name to be displayed. Default is `"Librespot"`. + pub fn name(mut self, name: impl Into>) -> Self { + self.server_config.name = name.into(); + self + } + + /// Sets the device type which is visible as icon in other Spotify clients. Default is `Speaker`. + pub fn device_type(mut self, device_type: DeviceType) -> Self { + self.server_config.device_type = device_type; + self + } + + /// Sets the port on which it should listen to incoming connections. + /// The default value `0` means any port. + pub fn port(mut self, port: u16) -> Self { + self.port = port; + self + } + + /// Sets up the [`Discovery`] instance. + /// + /// # Errors + /// If setting up the mdns service or creating the server fails, this function returns an error. + 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 svc; + + cfg_if! { + if #[cfg(feature = "with-dns-sd")] { + svc = dns_sd::DNSService::register( + Some(name.as_ref()), + "_spotify-connect._tcp", + None, + None, + port, + &["VERSION=1.0", "CPath=/"], + ) + .unwrap(); + + } else { + let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; + svc = responder.register( + "_spotify-connect._tcp".to_owned(), + name, + port, + &["VERSION=1.0", "CPath=/"], + ) + } + }; + + Ok(Discovery { server, _svc: svc }) + } +} + +impl Discovery { + /// Starts a [`Builder`] with the provided device id. + pub fn builder(device_id: impl Into) -> Builder { + Builder::new(device_id) + } + + /// Create a new instance with the specified device id and default paramaters. + pub fn new(device_id: impl Into) -> Result { + Self::builder(device_id).launch() + } +} + +impl Stream for Discovery { + type Item = Credentials; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.server).poll_next(cx) + } +} diff --git a/discovery/src/server.rs b/discovery/src/server.rs new file mode 100644 index 00000000..53b849f7 --- /dev/null +++ b/discovery/src/server.rs @@ -0,0 +1,236 @@ +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 aes_ctr::cipher::generic_array::GenericArray; +use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; +use aes_ctr::Aes128Ctr; +use futures_core::Stream; +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 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; + +type Params<'a> = BTreeMap, Cow<'a, str>>; + +pub struct Config { + pub name: Cow<'static, str>, + pub device_type: DeviceType, + pub device_id: String, +} + +struct RequestHandler { + config: Config, + keys: DhLocalKeys, + tx: mpsc::UnboundedSender, +} + +impl RequestHandler { + fn new(config: Config) -> (Self, mpsc::UnboundedReceiver) { + let (tx, rx) = mpsc::unbounded_channel(); + + let discovery = Self { + config, + keys: DhLocalKeys::random(&mut rand::thread_rng()), + tx, + }; + + (discovery, rx) + } + + fn handle_get_info(&self) -> Response { + let public_key = base64::encode(&self.keys.public_key()); + let device_type: &str = self.config.device_type.into(); + + let body = json!({ + "status": 101, + "statusString": "ERROR-OK", + "spotifyError": 0, + "version": "2.7.1", + "deviceID": (self.config.device_id), + "remoteName": (self.config.name), + "activeUser": "", + "publicKey": (public_key), + "deviceType": (device_type), + "libraryVersion": crate::core::version::SEMVER, + "accountReq": "PREMIUM", + "brandDisplayName": "librespot", + "modelDisplayName": "librespot", + "resolverVersion": "0", + "groupStatus": "NONE", + "voiceSupport": "NO", + }) + .to_string(); + + 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(); + + let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); + + let client_key = base64::decode(client_key.as_bytes()).unwrap(); + let shared_key = self.keys.shared_secret(&client_key); + + let iv = &encrypted_blob[0..16]; + let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; + let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; + + let base_key = Sha1::digest(&shared_key); + 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"); + 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"); + 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"); + h.update(encrypted); + if h.verify(cksum).is_err() { + warn!("Login error for user {:?}: MAC mismatch", username); + let result = json!({ + "status": 102, + "spotifyError": 1, + "statusString": "ERROR-MAC" + }); + + let body = result.to_string(); + return Response::new(Body::from(body)); + } + + let decrypted = { + let mut data = encrypted.to_vec(); + let mut cipher = Aes128Ctr::new( + GenericArray::from_slice(&encryption_key[0..16]), + GenericArray::from_slice(iv), + ); + cipher.apply_keystream(&mut data); + String::from_utf8(data).unwrap() + }; + + let credentials = + Credentials::with_blob(username.to_string(), &decrypted, &self.config.device_id); + + self.tx.send(credentials).unwrap(); + + let result = json!({ + "status": 101, + "spotifyError": 0, + "statusString": "ERROR-OK" + }); + + let body = result.to_string(); + Response::new(Body::from(body)) + } + + fn not_found(&self) -> Response { + let mut res = Response::default(); + *res.status_mut() = StatusCode::NOT_FOUND; + res + } + + async fn handle(self: Arc, request: Request) -> hyper::Result> { + let mut params = Params::new(); + + let (parts, body) = request.into_parts(); + + if let Some(query) = parts.uri.query() { + let query_params = form_urlencoded::parse(query.as_bytes()); + params.extend(query_params); + } + + if parts.method != Method::GET { + debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params); + } + + let body = hyper::body::to_bytes(body).await?; + + params.extend(form_urlencoded::parse(&body)); + + let action = params.get("action").map(Cow::as_ref); + + Ok(match (parts.method, action) { + (Method::GET, Some("getInfo")) => self.handle_get_info(), + (Method::POST, Some("addUser")) => self.handle_add_user(¶ms), + _ => self.not_found(), + }) + } +} + +pub struct DiscoveryServer { + cred_rx: mpsc::UnboundedReceiver, + _close_tx: oneshot::Sender, +} + +impl DiscoveryServer { + pub fn new(config: Config, port: &mut u16) -> hyper::Result { + let (discovery, cred_rx) = RequestHandler::new(config); + let discovery = Arc::new(discovery); + + let (close_tx, close_rx) = oneshot::channel(); + + let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port); + + let make_service = make_service_fn(move |_| { + let discovery = discovery.clone(); + async move { + Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().handle(request))) + } + }); + + let server = hyper::Server::try_bind(&address)?.serve(make_service); + + *port = server.local_addr().port(); + debug!("Zeroconf server listening on 0.0.0.0:{}", *port); + + tokio::spawn(async { + let result = server + .with_graceful_shutdown(async { + close_rx.await.unwrap_err(); + debug!("Shutting down discovery server"); + }) + .await; + + if let Err(e) = result { + warn!("Discovery server failed: {}", e); + } + }); + + Ok(Self { + cred_rx, + _close_tx: close_tx, + }) + } +} + +impl Stream for DiscoveryServer { + type Item = Credentials; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.cred_rx.poll_recv(cx) + } +} diff --git a/examples/get_token.rs b/examples/get_token.rs index 636155e0..3ef6bd71 100644 --- a/examples/get_token.rs +++ b/examples/get_token.rs @@ -2,7 +2,6 @@ use std::env; use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; -use librespot::core::keymaster; use librespot::core::session::Session; const SCOPES: &str = @@ -13,8 +12,8 @@ async fn main() { let session_config = SessionConfig::default(); let args: Vec<_> = env::args().collect(); - if args.len() != 4 { - eprintln!("Usage: {} USERNAME PASSWORD CLIENT_ID", args[0]); + if args.len() != 3 { + eprintln!("Usage: {} USERNAME PASSWORD", args[0]); return; } @@ -26,8 +25,6 @@ async fn main() { println!( "Token: {:#?}", - keymaster::get_token(&session, &args[3], SCOPES) - .await - .unwrap() + session.token_provider().get_token(SCOPES).await.unwrap() ); } diff --git a/metadata/src/cover.rs b/metadata/src/cover.rs index 408e658e..b483f454 100644 --- a/metadata/src/cover.rs +++ b/metadata/src/cover.rs @@ -2,6 +2,7 @@ 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; @@ -13,7 +14,7 @@ pub fn get(session: &Session, file: FileId) -> ChannelData { packet.write_u16::(channel_id).unwrap(); packet.write_u16::(0).unwrap(); packet.write(&file.0).unwrap(); - session.send_packet(0x19, packet); + session.send_packet(PacketType::Image, packet); data } diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 37806062..0bed793c 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -18,15 +18,15 @@ path = "../metadata" version = "0.2.0" [dependencies] -cfg-if = "1.0" futures-executor = "0.3" 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"] } -zerocopy = { version = "0.3" } +zerocopy = { version = "0.3" } +# Backends alsa = { version = "0.5", optional = true } portaudio-rs = { version = "0.3", optional = true } libpulse-binding = { version = "2", optional = true, default-features = false } @@ -42,14 +42,16 @@ rodio = { version = "0.14", optional = true, default-features = false cpal = { version = "0.13", optional = true } thiserror = { version = "1", optional = true } -# Decoders -lewton = "0.10" # Currently not optional because of limitations of cargo features -librespot-tremor = { version = "0.2", optional = true } +# Decoder +lewton = "0.10" ogg = "0.8" -vorbis = { version ="0.0", optional = true } + +# Dithering +rand = "0.8" +rand_distr = "0.4" [features] -alsa-backend = ["alsa"] +alsa-backend = ["alsa", "thiserror"] portaudio-backend = ["portaudio-rs"] pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] jackaudio-backend = ["jack"] @@ -57,6 +59,3 @@ rodio-backend = ["rodio", "cpal", "thiserror"] rodiojack-backend = ["rodio", "cpal/jack", "thiserror"] sdl-backend = ["sdl2"] gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] - -with-tremor = ["librespot-tremor"] -with-vorbis = ["vorbis"] \ No newline at end of file diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index c7bc4e55..7101f96d 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,95 +1,189 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; use alsa::device_name::HintIter; -use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; -use alsa::{Direction, Error, ValueOr}; +use alsa::pcm::{Access, Format, HwParams, PCM}; +use alsa::{Direction, ValueOr}; use std::cmp::min; -use std::ffi::CString; use std::io; use std::process::exit; +use std::time::Duration; +use thiserror::Error; -const BUFFERED_LATENCY: f32 = 0.125; // seconds -const BUFFERED_PERIODS: Frames = 4; +// 125 ms Period time * 4 periods = 0.5 sec buffer. +const PERIOD_TIME: Duration = Duration::from_millis(125); +const NUM_PERIODS: u32 = 4; + +#[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}")] + UnsupportedFormat { + device: String, + format: AudioFormat, + err: 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}")] + UnsupportedChannelCount { + device: String, + channel_count: u8, + err: alsa::Error, + }, + #[error("AlsaSink Hardware Parameters Error, {0}")] + HwParams(alsa::Error), + #[error("AlsaSink Software Parameters Error, {0}")] + SwParams(alsa::Error), + #[error("AlsaSink PCM Error, {0}")] + Pcm(alsa::Error), +} pub struct AlsaSink { pcm: Option, format: AudioFormat, device: String, - buffer: Vec, + period_buffer: Vec, } -fn list_outputs() { +fn list_outputs() -> io::Result<()> { + println!("Listing available Alsa outputs:"); for t in &["pcm", "ctl", "hwdep"] { println!("{} devices:", t); - let i = HintIter::new(None, &*CString::new(*t).unwrap()).unwrap(); + let i = match HintIter::new_str(None, &t) { + Ok(i) => i, + Err(e) => { + return Err(io::Error::new(io::ErrorKind::Other, e)); + } + }; for a in i { if let Some(Direction::Playback) = a.direction { // mimic aplay -L - println!( - "{}\n\t{}\n", - a.name.unwrap(), - a.desc.unwrap().replace("\n", "\n\t") - ); + 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"))?; + println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t")); } } } + + Ok(()) } -fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box> { - let pcm = PCM::new(dev_name, Direction::Playback, false)?; +fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), AlsaError> { + let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp { + device: dev_name.to_string(), + err: e, + })?; + let alsa_format = match format { + AudioFormat::F64 => Format::float64(), AudioFormat::F32 => Format::float(), AudioFormat::S32 => Format::s32(), AudioFormat::S24 => Format::s24(), - AudioFormat::S24_3 => Format::S243LE, AudioFormat::S16 => Format::s16(), + + #[cfg(target_endian = "little")] + AudioFormat::S24_3 => Format::S243LE, + #[cfg(target_endian = "big")] + AudioFormat::S24_3 => Format::S243BE, }; - // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8 - // latency = period_size * periods / (rate * bytes_per_frame) - // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. - let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32 - * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames; - { - let hwp = HwParams::any(&pcm)?; - hwp.set_access(Access::RWInterleaved)?; - hwp.set_format(alsa_format)?; - hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?; - hwp.set_channels(NUM_CHANNELS as u32)?; - period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?; - hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?; - pcm.hw_params(&hwp)?; + 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, + })?; - let swp = pcm.sw_params_current()?; - swp.set_start_threshold(hwp.get_buffer_size()? - hwp.get_period_size()?)?; - pcm.sw_params(&swp)?; - } + hwp.set_format(alsa_format) + .map_err(|e| AlsaError::UnsupportedFormat { + device: dev_name.to_string(), + format, + err: e, + })?; - Ok((pcm, period_size)) + hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| { + AlsaError::UnsupportedSampleRate { + device: dev_name.to_string(), + samplerate: SAMPLE_RATE, + err: e, + } + })?; + + hwp.set_channels(NUM_CHANNELS as u32) + .map_err(|e| AlsaError::UnsupportedChannelCount { + device: dev_name.to_string(), + channel_count: NUM_CHANNELS, + err: e, + })?; + + // Deal strictly in time and periods. + hwp.set_periods(NUM_PERIODS, ValueOr::Nearest) + .map_err(AlsaError::HwParams)?; + + hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest) + .map_err(AlsaError::HwParams)?; + + pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; + + let swp = pcm.sw_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)?; + + swp.set_start_threshold(frames_per_buffer - frames_per_period) + .map_err(AlsaError::SwParams)?; + + pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; + + // Let ALSA do the math for us. + pcm.frames_to_bytes(frames_per_period) as usize + }; + + Ok((pcm, bytes_per_period)) } impl Open for AlsaSink { fn open(device: Option, format: AudioFormat) -> Self { - info!("Using Alsa sink with format: {:?}", format); - - let name = match device.as_ref().map(AsRef::as_ref) { - Some("?") => { - println!("Listing available Alsa outputs:"); - list_outputs(); - exit(0) - } + let name = match device.as_deref() { + Some("?") => match list_outputs() { + Ok(_) => { + exit(0); + } + Err(err) => { + error!("Error listing Alsa outputs, {}", err); + exit(1); + } + }, Some(device) => device, None => "default", } .to_string(); + info!("Using AlsaSink with format: {:?}", format); + Self { pcm: None, format, device: name, - buffer: vec![], + period_buffer: vec![], } } } @@ -97,21 +191,13 @@ impl Open for AlsaSink { impl Sink for AlsaSink { fn start(&mut self) -> io::Result<()> { if self.pcm.is_none() { - let pcm = open_device(&self.device, self.format); - match pcm { - Ok((p, period_size)) => { - self.pcm = Some(p); - // Create a buffer for all samples for a full period - self.buffer = Vec::with_capacity( - period_size as usize * BUFFERED_PERIODS as usize * self.format.size(), - ); + 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); } Err(e) => { - error!("Alsa error PCM open {}", e); - return Err(io::Error::new( - io::ErrorKind::Other, - "Alsa error: PCM open failed", - )); + return Err(io::Error::new(io::ErrorKind::Other, e)); } } } @@ -123,9 +209,16 @@ impl Sink for AlsaSink { { // Write any leftover data in the period buffer // before draining the actual buffer - self.write_bytes(&[]).expect("could not flush buffer"); - let pcm = self.pcm.as_mut().unwrap(); - pcm.drain().unwrap(); + 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), + ) + })? } self.pcm = None; Ok(()) @@ -139,15 +232,15 @@ impl SinkAsBytes for AlsaSink { let mut processed_data = 0; while processed_data < data.len() { let data_to_buffer = min( - self.buffer.capacity() - self.buffer.len(), + self.period_buffer.capacity() - self.period_buffer.len(), data.len() - processed_data, ); - self.buffer + self.period_buffer .extend_from_slice(&data[processed_data..processed_data + data_to_buffer]); processed_data += data_to_buffer; - if self.buffer.len() == self.buffer.capacity() { - self.write_buf(); - self.buffer.clear(); + if self.period_buffer.len() == self.period_buffer.capacity() { + self.write_buf()?; + self.period_buffer.clear(); } } @@ -156,12 +249,34 @@ impl SinkAsBytes for AlsaSink { } impl AlsaSink { - fn write_buf(&mut self) { - let pcm = self.pcm.as_mut().unwrap(); + 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(); - match io.writei(&self.buffer) { - Ok(_) => (), - Err(err) => pcm.try_recover(err, false).unwrap(), - }; + if let Err(err) = io.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 + ); + 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 + ), + ) + })? + } + + Ok(()) } } diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index e31c66ae..58f6cbc9 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,7 +1,8 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; use gstreamer as gst; use gstreamer_app as gst_app; @@ -33,11 +34,17 @@ impl Open for GstreamerSink { let sample_size = format.size(); let gst_bytes = 2048 * sample_size; + #[cfg(target_endian = "little")] + const ENDIANNESS: &str = "LE"; + #[cfg(target_endian = "big")] + const ENDIANNESS: &str = "BE"; + let pipeline_str_preamble = format!( - "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ", - gst_format, NUM_CHANNELS, SAMPLE_RATE, gst_bytes + "appsrc caps=\"audio/x-raw,format={}{},layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ", + gst_format, ENDIANNESS, NUM_CHANNELS, SAMPLE_RATE, gst_bytes ); - let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; + // no need to dither twice; use librespot dithering instead + let pipeline_str_rest = r#" ! audioconvert dithering=none ! autoaudiosink"#; let pipeline_str: String = match device { Some(x) => format!("{}{}", pipeline_str_preamble, x), None => format!("{}{}", pipeline_str_preamble, pipeline_str_rest), @@ -120,7 +127,6 @@ impl Open for GstreamerSink { } impl Sink for GstreamerSink { - start_stop_noop!(); sink_as_bytes!(); } @@ -133,3 +139,7 @@ impl SinkAsBytes for GstreamerSink { Ok(()) } } + +impl GstreamerSink { + pub const NAME: &'static str = "gstreamer"; +} diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 816147ff..f55f20a8 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,7 +1,8 @@ use super::{Open, Sink}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::NUM_CHANNELS; +use crate::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; @@ -69,11 +70,10 @@ impl Open for JackSink { } impl Sink for JackSink { - start_stop_noop!(); - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - for s in packet.samples().iter() { - let res = self.send.send(*s); + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + let samples_f32: &[f32] = &converter.f64_to_f32(packet.samples()); + for sample in samples_f32.iter() { + let res = self.send.send(*sample); if res.is_err() { error!("cannot write to channel"); } @@ -81,3 +81,7 @@ impl Sink for JackSink { Ok(()) } } + +impl JackSink { + pub const NAME: &'static str = "jackaudio"; +} diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 84e35634..31fb847c 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,4 +1,5 @@ use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; use std::io; @@ -7,9 +8,13 @@ pub trait Open { } pub trait Sink { - fn start(&mut self) -> io::Result<()>; - fn stop(&mut self) -> io::Result<()>; - fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; + fn start(&mut self) -> io::Result<()> { + Ok(()) + } + fn stop(&mut self) -> io::Result<()> { + Ok(()) + } + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()>; } pub type SinkBuilder = fn(Option, AudioFormat) -> Box; @@ -25,26 +30,30 @@ fn mk_sink(device: Option, format: AudioFormat // reuse code for various backends macro_rules! sink_as_bytes { () => { - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - use crate::convert::{self, i24}; + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + use crate::convert::i24; use zerocopy::AsBytes; match packet { AudioPacket::Samples(samples) => match self.format { - AudioFormat::F32 => self.write_bytes(samples.as_bytes()), + AudioFormat::F64 => self.write_bytes(samples.as_bytes()), + AudioFormat::F32 => { + let samples_f32: &[f32] = &converter.f64_to_f32(samples); + self.write_bytes(samples_f32.as_bytes()) + } AudioFormat::S32 => { - let samples_s32: &[i32] = &convert::to_s32(samples); + let samples_s32: &[i32] = &converter.f64_to_s32(samples); self.write_bytes(samples_s32.as_bytes()) } AudioFormat::S24 => { - let samples_s24: &[i32] = &convert::to_s24(samples); + let samples_s24: &[i32] = &converter.f64_to_s24(samples); self.write_bytes(samples_s24.as_bytes()) } AudioFormat::S24_3 => { - let samples_s24_3: &[i24] = &convert::to_s24_3(samples); + let samples_s24_3: &[i24] = &converter.f64_to_s24_3(samples); self.write_bytes(samples_s24_3.as_bytes()) } AudioFormat::S16 => { - let samples_s16: &[i16] = &convert::to_s16(samples); + let samples_s16: &[i16] = &converter.f64_to_s16(samples); self.write_bytes(samples_s16.as_bytes()) } }, @@ -54,17 +63,6 @@ macro_rules! sink_as_bytes { }; } -macro_rules! start_stop_noop { - () => { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } - }; -} - #[cfg(feature = "alsa-backend")] mod alsa; #[cfg(feature = "alsa-backend")] @@ -92,6 +90,8 @@ use self::gstreamer::GstreamerSink; #[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] mod rodio; +#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] +use self::rodio::RodioSink; #[cfg(feature = "sdl-backend")] mod sdl; @@ -105,24 +105,24 @@ mod subprocess; use self::subprocess::SubprocessSink; pub const BACKENDS: &[(&str, SinkBuilder)] = &[ - #[cfg(feature = "alsa-backend")] - ("alsa", mk_sink::), - #[cfg(feature = "portaudio-backend")] - ("portaudio", mk_sink::), - #[cfg(feature = "pulseaudio-backend")] - ("pulseaudio", mk_sink::), - #[cfg(feature = "jackaudio-backend")] - ("jackaudio", mk_sink::), - #[cfg(feature = "gstreamer-backend")] - ("gstreamer", mk_sink::), #[cfg(feature = "rodio-backend")] - ("rodio", rodio::mk_rodio), + (RodioSink::NAME, rodio::mk_rodio), // default goes first + #[cfg(feature = "alsa-backend")] + (AlsaSink::NAME, mk_sink::), + #[cfg(feature = "portaudio-backend")] + (PortAudioSink::NAME, mk_sink::), + #[cfg(feature = "pulseaudio-backend")] + (PulseAudioSink::NAME, mk_sink::), + #[cfg(feature = "jackaudio-backend")] + (JackSink::NAME, mk_sink::), + #[cfg(feature = "gstreamer-backend")] + (GstreamerSink::NAME, mk_sink::), #[cfg(feature = "rodiojack-backend")] ("rodiojack", rodio::mk_rodiojack), #[cfg(feature = "sdl-backend")] - ("sdl", mk_sink::), - ("pipe", mk_sink::), - ("subprocess", mk_sink::), + (SdlSink::NAME, mk_sink::), + (StdoutSink::NAME, mk_sink::), + (SubprocessSink::NAME, mk_sink::), ]; pub fn find(name: Option) -> Option { diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index df3e6c0f..56040384 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,36 +1,66 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; use std::fs::OpenOptions; use std::io::{self, Write}; pub struct StdoutSink { - output: Box, + output: Option>, + path: Option, format: AudioFormat, } impl Open for StdoutSink { fn open(path: Option, format: AudioFormat) -> Self { info!("Using pipe sink with format: {:?}", format); - - let output: Box = match path { - Some(path) => Box::new(OpenOptions::new().write(true).open(path).unwrap()), - _ => Box::new(io::stdout()), - }; - - Self { output, format } + Self { + output: None, + path, + format, + } } } impl Sink for StdoutSink { - start_stop_noop!(); + fn start(&mut self) -> io::Result<()> { + 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))?; + Box::new(open_op) + } + None => Box::new(io::stdout()), + }; + + self.output = Some(output); + } + + Ok(()) + } + sink_as_bytes!(); } impl SinkAsBytes for StdoutSink { fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { - self.output.write_all(data)?; - self.output.flush()?; + match self.output.as_deref_mut() { + Some(output) => { + output.write_all(data)?; + output.flush()?; + } + None => { + return Err(io::Error::new(io::ErrorKind::Other, "Output is None")); + } + } + Ok(()) } } + +impl StdoutSink { + pub const NAME: &'static str = "pipe"; +} diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 4fe471a9..378deb48 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,8 +1,8 @@ use super::{Open, Sink}; use crate::config::AudioFormat; -use crate::convert; +use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; use std::io; @@ -55,12 +55,9 @@ impl<'a> Open for PortAudioSink<'a> { fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { info!("Using PortAudio sink with format: {:?}", format); - warn!("This backend is known to panic on several platforms."); - warn!("Consider using some other backend, or better yet, contributing a fix."); - portaudio_rs::initialize().unwrap(); - let device_idx = match device.as_ref().map(AsRef::as_ref) { + let device_idx = match device.as_deref() { Some("?") => { list_outputs(); exit(0) @@ -109,7 +106,7 @@ impl<'a> Sink for PortAudioSink<'a> { Some(*$parameters), SAMPLE_RATE as f64, FRAMES_PER_BUFFER_UNSPECIFIED, - StreamFlags::empty(), + StreamFlags::DITHER_OFF, // no need to dither twice; use librespot dithering instead None, ) .unwrap(), @@ -136,15 +133,15 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - Self::F32(stream, _parameters) => stop_sink!(ref mut stream), - Self::S32(stream, _parameters) => stop_sink!(ref mut stream), - Self::S16(stream, _parameters) => stop_sink!(ref mut stream), + Self::F32(stream, _) => stop_sink!(ref mut stream), + Self::S32(stream, _) => stop_sink!(ref mut stream), + Self::S16(stream, _) => stop_sink!(ref mut stream), }; Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { macro_rules! write_sink { (ref mut $stream: expr, $samples: expr) => { $stream.as_mut().unwrap().write($samples) @@ -154,14 +151,15 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet.samples(); let result = match self { Self::F32(stream, _parameters) => { - write_sink!(ref mut stream, samples) + let samples_f32: &[f32] = &converter.f64_to_f32(samples); + write_sink!(ref mut stream, samples_f32) } Self::S32(stream, _parameters) => { - let samples_s32: &[i32] = &convert::to_s32(samples); + let samples_s32: &[i32] = &converter.f64_to_s32(samples); write_sink!(ref mut stream, samples_s32) } Self::S16(stream, _parameters) => { - let samples_s16: &[i16] = &convert::to_s16(samples); + let samples_s16: &[i16] = &converter.f64_to_s16(samples); write_sink!(ref mut stream, samples_s16) } }; @@ -180,3 +178,7 @@ impl<'a> Drop for PortAudioSink<'a> { portaudio_rs::terminate().unwrap(); } } + +impl<'a> PortAudioSink<'a> { + pub const NAME: &'static str = "portaudio"; +} diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 90a4a67a..e36941ea 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,7 +1,8 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_simple_binding::Simple; use std::io; @@ -22,11 +23,14 @@ impl Open for PulseAudioSink { // PulseAudio calls S24 and S24_3 different from the rest of the world let pulse_format = match format { - AudioFormat::F32 => pulse::sample::Format::F32le, - AudioFormat::S32 => pulse::sample::Format::S32le, - AudioFormat::S24 => pulse::sample::Format::S24_32le, - AudioFormat::S24_3 => pulse::sample::Format::S24le, - AudioFormat::S16 => pulse::sample::Format::S16le, + 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) + } }; let ss = pulse::sample::Spec { @@ -51,7 +55,7 @@ impl Sink for PulseAudioSink { return Ok(()); } - let device = self.device.as_ref().map(|s| (*s).as_str()); + let device = self.device.as_deref(); let result = Simple::new( None, // Use the default server. APP_NAME, // Our application's name. @@ -100,3 +104,7 @@ impl SinkAsBytes for PulseAudioSink { } } } + +impl PulseAudioSink { + pub const NAME: &'static str = "pulseaudio"; +} diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 9399a309..1e999938 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,14 +1,15 @@ use std::process::exit; -use std::{io, thread, time}; +use std::time::Duration; +use std::{io, thread}; use cpal::traits::{DeviceTrait, HostTrait}; use thiserror::Error; use super::Sink; use crate::config::AudioFormat; -use crate::convert; +use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; #[cfg(all( feature = "rodiojack-backend", @@ -174,18 +175,20 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro } impl Sink for RodioSink { - start_stop_noop!(); - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { let samples = packet.samples(); match self.format { AudioFormat::F32 => { - let source = - rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); + let samples_f32: &[f32] = &converter.f64_to_f32(samples); + let source = rodio::buffer::SamplesBuffer::new( + NUM_CHANNELS as u16, + SAMPLE_RATE, + samples_f32, + ); self.rodio_sink.append(source); } AudioFormat::S16 => { - let samples_s16: &[i16] = &convert::to_s16(samples); + let samples_s16: &[i16] = &converter.f64_to_s16(samples); let source = rodio::buffer::SamplesBuffer::new( NUM_CHANNELS as u16, SAMPLE_RATE, @@ -201,8 +204,12 @@ impl Sink for RodioSink { // 44100 elements --> about 27 chunks while self.rodio_sink.len() > 26 { // sleep and wait for rodio to drain a bit - thread::sleep(time::Duration::from_millis(10)); + thread::sleep(Duration::from_millis(10)); } Ok(()) } } + +impl RodioSink { + pub const NAME: &'static str = "rodio"; +} diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index a3a608d9..28d140e8 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,10 +1,11 @@ use super::{Open, Sink}; use crate::config::AudioFormat; -use crate::convert; +use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; -use std::{io, thread, time}; +use std::time::Duration; +use std::{io, thread}; pub enum SdlSink { F32(AudioQueue), @@ -81,12 +82,12 @@ impl Sink for SdlSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { macro_rules! drain_sink { ($queue: expr, $size: expr) => {{ // sleep and wait for sdl thread to drain the queue a bit while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) { - thread::sleep(time::Duration::from_millis(10)); + thread::sleep(Duration::from_millis(10)); } }}; } @@ -94,16 +95,17 @@ impl Sink for SdlSink { let samples = packet.samples(); match self { Self::F32(queue) => { + let samples_f32: &[f32] = &converter.f64_to_f32(samples); drain_sink!(queue, AudioFormat::F32.size()); - queue.queue(samples) + queue.queue(samples_f32) } Self::S32(queue) => { - let samples_s32: &[i32] = &convert::to_s32(samples); + let samples_s32: &[i32] = &converter.f64_to_s32(samples); drain_sink!(queue, AudioFormat::S32.size()); queue.queue(samples_s32) } Self::S16(queue) => { - let samples_s16: &[i16] = &convert::to_s16(samples); + let samples_s16: &[i16] = &converter.f64_to_s16(samples); drain_sink!(queue, AudioFormat::S16.size()); queue.queue(samples_s16) } @@ -111,3 +113,7 @@ impl Sink for SdlSink { Ok(()) } } + +impl SdlSink { + pub const NAME: &'static str = "sdl"; +} diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index f493e7a7..64f04c88 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,5 +1,6 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; +use crate::convert::Converter; use crate::decoder::AudioPacket; use shell_words::split; @@ -61,3 +62,7 @@ impl SinkAsBytes for SubprocessSink { Ok(()) } } + +impl SubprocessSink { + pub const NAME: &'static str = "subprocess"; +} diff --git a/playback/src/config.rs b/playback/src/config.rs index feb1d61e..7604f59f 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,9 +1,10 @@ -use super::player::NormalisationData; +use super::player::db_to_ratio; use crate::convert::i24; +pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; -use std::convert::TryFrom; use std::mem; use std::str::FromStr; +use std::time::Duration; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum Bitrate { @@ -32,6 +33,7 @@ impl Default for Bitrate { #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum AudioFormat { + F64, F32, S32, S24, @@ -39,10 +41,11 @@ pub enum AudioFormat { S16, } -impl TryFrom<&String> for AudioFormat { - type Error = (); - fn try_from(s: &String) -> Result { - match s.to_uppercase().as_str() { +impl FromStr for AudioFormat { + type Err = (); + fn from_str(s: &str) -> Result { + match s.to_uppercase().as_ref() { + "F64" => Ok(Self::F64), "F32" => Ok(Self::F32), "S32" => Ok(Self::S32), "S24" => Ok(Self::S24), @@ -64,6 +67,8 @@ impl AudioFormat { #[allow(dead_code)] pub fn size(&self) -> usize { match self { + Self::F64 => mem::size_of::(), + Self::F32 => mem::size_of::(), Self::S24_3 => mem::size_of::(), Self::S16 => mem::size_of::(), _ => mem::size_of::(), // S32 and S24 are both stored in i32 @@ -80,7 +85,7 @@ pub enum NormalisationType { impl FromStr for NormalisationType { type Err = (); fn from_str(s: &str) -> Result { - match s { + match s.to_lowercase().as_ref() { "album" => Ok(Self::Album), "track" => Ok(Self::Track), _ => Err(()), @@ -103,7 +108,7 @@ pub enum NormalisationMethod { impl FromStr for NormalisationMethod { type Err = (); fn from_str(s: &str) -> Result { - match s { + match s.to_lowercase().as_ref() { "basic" => Ok(Self::Basic), "dynamic" => Ok(Self::Dynamic), _ => Err(()), @@ -117,35 +122,81 @@ impl Default for NormalisationMethod { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct PlayerConfig { pub bitrate: Bitrate, + pub gapless: bool, + pub passthrough: bool, + pub normalisation: bool, pub normalisation_type: NormalisationType, pub normalisation_method: NormalisationMethod, - pub normalisation_pregain: f32, - pub normalisation_threshold: f32, - pub normalisation_attack: f32, - pub normalisation_release: f32, - pub normalisation_knee: f32, - pub gapless: bool, - pub passthrough: bool, + pub normalisation_pregain: f64, + pub normalisation_threshold: f64, + pub normalisation_attack: Duration, + pub normalisation_release: Duration, + pub normalisation_knee: f64, + + // pass function pointers so they can be lazily instantiated *after* spawning a thread + // (thereby circumventing Send bounds that they might not satisfy) + pub ditherer: Option, } impl Default for PlayerConfig { - fn default() -> PlayerConfig { - PlayerConfig { + fn default() -> Self { + Self { bitrate: Bitrate::default(), + gapless: true, normalisation: false, normalisation_type: NormalisationType::default(), normalisation_method: NormalisationMethod::default(), normalisation_pregain: 0.0, - normalisation_threshold: NormalisationData::db_to_ratio(-1.0), - normalisation_attack: 0.005, - normalisation_release: 0.1, + normalisation_threshold: db_to_ratio(-1.0), + normalisation_attack: Duration::from_millis(5), + normalisation_release: Duration::from_millis(100), normalisation_knee: 1.0, - gapless: true, passthrough: false, + ditherer: Some(mk_ditherer::), + } + } +} + +// fields are intended for volume control range in dB +#[derive(Clone, Copy, Debug)] +pub enum VolumeCtrl { + Cubic(f64), + Fixed, + Linear, + Log(f64), +} + +impl FromStr for VolumeCtrl { + type Err = (); + fn from_str(s: &str) -> Result { + Self::from_str_with_range(s, Self::DEFAULT_DB_RANGE) + } +} + +impl Default for VolumeCtrl { + fn default() -> VolumeCtrl { + VolumeCtrl::Log(Self::DEFAULT_DB_RANGE) + } +} + +impl VolumeCtrl { + pub const MAX_VOLUME: u16 = u16::MAX; + + // Taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html + pub const DEFAULT_DB_RANGE: f64 = 60.0; + + pub fn from_str_with_range(s: &str, db_range: f64) -> Result::Err> { + use self::VolumeCtrl::*; + match s.to_lowercase().as_ref() { + "cubic" => Ok(Cubic(db_range)), + "fixed" => Ok(Fixed), + "linear" => Ok(Linear), + "log" => Ok(Log(db_range)), + _ => Err(()), } } } diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 450910b0..962ade66 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -1,3 +1,4 @@ +use crate::dither::{Ditherer, DithererBuilder}; use zerocopy::AsBytes; #[derive(AsBytes, Copy, Clone, Debug)] @@ -5,52 +6,122 @@ use zerocopy::AsBytes; #[repr(transparent)] pub struct i24([u8; 3]); impl i24 { - fn pcm_from_i32(sample: i32) -> Self { - // drop the least significant byte - let [a, b, c, _d] = (sample >> 8).to_le_bytes(); - i24([a, b, c]) + fn from_s24(sample: i32) -> Self { + // trim the padding in the most significant byte + #[allow(unused_variables)] + let [a, b, c, d] = sample.to_ne_bytes(); + #[cfg(target_endian = "little")] + return Self([a, b, c]); + #[cfg(target_endian = "big")] + return Self([b, c, d]); } } -// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. -macro_rules! convert_samples_to { - ($type: ident, $samples: expr) => { - convert_samples_to!($type, $samples, 0) - }; - ($type: ident, $samples: expr, $drop_bits: expr) => { - $samples +pub struct Converter { + ditherer: Option>, +} + +impl Converter { + pub fn new(dither_config: Option) -> Self { + if let Some(ref ditherer_builder) = dither_config { + let ditherer = (ditherer_builder)(); + info!("Converting with ditherer: {}", ditherer.name()); + Self { + ditherer: Some(ditherer), + } + } else { + Self { ditherer: None } + } + } + + /// To convert PCM samples from floating point normalized as `-1.0..=1.0` + /// to 32-bit signed integer, multiply by 2147483648 (0x80000000) and + /// saturate at the bounds of `i32`. + const SCALE_S32: f64 = 2147483648.; + + /// To convert PCM samples from floating point normalized as `-1.0..=1.0` + /// to 24-bit signed integer, multiply by 8388608 (0x800000) and saturate + /// at the bounds of `i24`. + const SCALE_S24: f64 = 8388608.; + + /// To convert PCM samples from floating point normalized as `-1.0..=1.0` + /// to 16-bit signed integer, multiply by 32768 (0x8000) and saturate at + /// the bounds of `i16`. When the samples were encoded using the same + /// scaling factor, like the reference Vorbis encoder does, this makes + /// conversions transparent. + const SCALE_S16: f64 = 32768.; + + pub fn scale(&mut self, sample: f64, factor: f64) -> f64 { + let dither = match self.ditherer { + Some(ref mut d) => d.noise(), + None => 0.0, + }; + + // From the many float to int conversion methods available, match what + // the reference Vorbis implementation uses: sample * 32768 (for 16 bit) + let int_value = sample * factor + dither; + + // Casting float to integer rounds towards zero by default, i.e. it + // truncates, and that generates larger error than rounding to nearest. + int_value.round() + } + + // Special case for samples packed in a word of greater bit depth (e.g. + // S24): clamp between min and max to ensure that the most significant + // byte is zero. Otherwise, dithering may cause an overflow. This is not + // necessary for other formats, because casting to integer will saturate + // to the bounds of the primitive. + pub fn clamping_scale(&mut self, sample: f64, factor: f64) -> f64 { + let int_value = self.scale(sample, factor); + + // In two's complement, there are more negative than positive values. + let min = -factor; + let max = factor - 1.0; + + if int_value < min { + return min; + } else if int_value > max { + return max; + } + int_value + } + + pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec { + samples.iter().map(|sample| *sample as f32).collect() + } + + pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec { + samples + .iter() + .map(|sample| self.scale(*sample, Self::SCALE_S32) as i32) + .collect() + } + + // S24 is 24-bit PCM packed in an upper 32-bit word + pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec { + samples + .iter() + .map(|sample| self.clamping_scale(*sample, Self::SCALE_S24) as i32) + .collect() + } + + // S24_3 is 24-bit PCM in a 3-byte array + pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec { + samples .iter() .map(|sample| { - // Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] - // while maintaining DC linearity. There is nothing to be gained - // by doing this in f64, as the significand of a f32 is 24 bits, - // just like the maximum bit depth we are converting to. - let int_value = *sample * (std::$type::MAX as f32 + 0.5) - 0.5; - - // Casting floats to ints truncates by default, which results - // in larger quantization error than rounding arithmetically. - // Flooring is faster, but again with larger error. - int_value.round() as $type >> $drop_bits + // Not as DRY as calling f32_to_s24 first, but this saves iterating + // over all samples twice. + let int_value = self.clamping_scale(*sample, Self::SCALE_S24) as i32; + i24::from_s24(int_value) }) .collect() - }; -} + } -pub fn to_s32(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples) -} - -pub fn to_s24(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples, 8) -} - -pub fn to_s24_3(samples: &[f32]) -> Vec { - to_s32(samples) - .iter() - .map(|sample| i24::pcm_from_i32(*sample)) - .collect() -} - -pub fn to_s16(samples: &[f32]) -> Vec { - convert_samples_to!(i16, samples) + pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec { + samples + .iter() + .map(|sample| self.scale(*sample, Self::SCALE_S16) as i16) + .collect() + } } diff --git a/playback/src/decoder/lewton_decoder.rs b/playback/src/decoder/lewton_decoder.rs index 528d9344..adf63e2a 100644 --- a/playback/src/decoder/lewton_decoder.rs +++ b/playback/src/decoder/lewton_decoder.rs @@ -1,10 +1,12 @@ use super::{AudioDecoder, AudioError, AudioPacket}; use lewton::inside_ogg::OggStreamReader; +use lewton::samples::InterleavedSamples; use std::error; use std::fmt; use std::io::{Read, Seek}; +use std::time::Duration; pub struct VorbisDecoder(OggStreamReader); pub struct VorbisError(lewton::VorbisError); @@ -23,7 +25,7 @@ where R: Read + Seek, { fn seek(&mut self, ms: i64) -> Result<(), AudioError> { - let absgp = ms * 44100 / 1000; + 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())), @@ -35,11 +37,8 @@ where use lewton::OggReadError::NoCapturePatternFound; use lewton::VorbisError::{BadAudio, OggError}; loop { - match self - .0 - .read_dec_packet_generic::>() - { - Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))), + 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)) => (), diff --git a/playback/src/decoder/libvorbis_decoder.rs b/playback/src/decoder/libvorbis_decoder.rs deleted file mode 100644 index 6f9a68a3..00000000 --- a/playback/src/decoder/libvorbis_decoder.rs +++ /dev/null @@ -1,89 +0,0 @@ -#[cfg(feature = "with-tremor")] -use librespot_tremor as vorbis; - -use super::{AudioDecoder, AudioError, AudioPacket}; -use std::error; -use std::fmt; -use std::io::{Read, Seek}; - -pub struct VorbisDecoder(vorbis::Decoder); -pub struct VorbisError(vorbis::VorbisError); - -impl VorbisDecoder -where - R: Read + Seek, -{ - pub fn new(input: R) -> Result, VorbisError> { - Ok(VorbisDecoder(vorbis::Decoder::new(input)?)) - } -} - -impl AudioDecoder for VorbisDecoder -where - R: Read + Seek, -{ - #[cfg(not(feature = "with-tremor"))] - fn seek(&mut self, ms: i64) -> Result<(), AudioError> { - self.0.time_seek(ms as f64 / 1000f64)?; - Ok(()) - } - - #[cfg(feature = "with-tremor")] - fn seek(&mut self, ms: i64) -> Result<(), AudioError> { - self.0.time_seek(ms)?; - Ok(()) - } - - fn next_packet(&mut self) -> Result, AudioError> { - loop { - match self.0.packets().next() { - Some(Ok(packet)) => { - // Losslessly represent [-32768, 32767] to [-1.0, 1.0] while maintaining DC linearity. - return Ok(Some(AudioPacket::Samples( - packet - .data - .iter() - .map(|sample| { - ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32 - }) - .collect(), - ))); - } - None => return Ok(None), - - Some(Err(vorbis::VorbisError::Hole)) => (), - Some(Err(err)) => return Err(err.into()), - } - } - } -} - -impl From for VorbisError { - fn from(err: vorbis::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) - } -} - -impl From for AudioError { - fn from(err: vorbis::VorbisError) -> AudioError { - AudioError::VorbisError(VorbisError(err)) - } -} diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index 6108f00f..9641e8b3 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -1,27 +1,23 @@ use std::fmt; -use cfg_if::cfg_if; - -cfg_if! { - if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] { - mod libvorbis_decoder; - pub use libvorbis_decoder::{VorbisDecoder, VorbisError}; - } else { - mod lewton_decoder; - pub use lewton_decoder::{VorbisDecoder, VorbisError}; - } -} +mod lewton_decoder; +pub use lewton_decoder::{VorbisDecoder, VorbisError}; mod passthrough_decoder; pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; pub enum AudioPacket { - Samples(Vec), + Samples(Vec), OggData(Vec), } impl AudioPacket { - pub fn samples(&self) -> &[f32] { + pub fn samples_from_f32(f32_samples: Vec) -> Self { + let f64_samples = f32_samples.iter().map(|sample| *sample as f64).collect(); + AudioPacket::Samples(f64_samples) + } + + pub fn samples(&self) -> &[f64] { match self { AudioPacket::Samples(s) => s, AudioPacket::OggData(_) => panic!("can't return OggData on samples"), diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index e064cba3..7c1ad532 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -1,8 +1,10 @@ // Passthrough decoder for librespot use super::{AudioDecoder, AudioError, AudioPacket}; +use crate::SAMPLE_RATE; 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> @@ -12,7 +14,7 @@ where let pck: Packet = rdr.read_packet_expected()?; let pkt_type = pck.data[0]; - debug!("Vorbis header type{}", &pkt_type); + debug!("Vorbis header type {}", &pkt_type); if pkt_type != code { return Err(PassthroughError(OggReadError::InvalidData)); @@ -96,7 +98,10 @@ impl AudioDecoder for PassthroughDecoder { self.stream_serial += 1; // hard-coded to 44.1 kHz - match self.rdr.seek_absgp(None, (ms * 44100 / 1000) as u64) { + match self.rdr.seek_absgp( + None, + Duration::from_millis(ms as u64 * SAMPLE_RATE as u64).as_secs(), + ) { Ok(_) => { // need to set some offset for next_page() let pck = self.rdr.read_packet().unwrap().unwrap(); diff --git a/playback/src/dither.rs b/playback/src/dither.rs new file mode 100644 index 00000000..2510b886 --- /dev/null +++ b/playback/src/dither.rs @@ -0,0 +1,150 @@ +use rand::rngs::ThreadRng; +use rand_distr::{Distribution, Normal, Triangular, Uniform}; +use std::fmt; + +const NUM_CHANNELS: usize = 2; + +// Dithering lowers digital-to-analog conversion ("requantization") error, +// linearizing output, lowering distortion and replacing it with a constant, +// fixed noise level, which is more pleasant to the ear than the distortion. +// +// Guidance: +// +// * On S24, S24_3 and S24, the default is to use triangular dithering. +// Depending on personal preference you may use Gaussian dithering instead; +// it's not as good objectively, but it may be preferred subjectively if +// you are looking for a more "analog" sound akin to tape hiss. +// +// * Advanced users who know that they have a DAC without noise shaping have +// a third option: high-passed dithering, which is like triangular dithering +// except that it moves dithering noise up in frequency where it is less +// audible. Note: 99% of DACs are of delta-sigma design with noise shaping, +// so unless you have a multibit / R2R DAC, or otherwise know what you are +// doing, this is not for you. +// +// * Don't dither or shape noise on S32 or F32. On F32 it's not supported +// anyway (there are no integer conversions and so no rounding errors) and +// on S32 the noise level is so far down that it is simply inaudible even +// after volume normalisation and control. +// +pub trait Ditherer { + fn new() -> Self + where + Self: Sized; + fn name(&self) -> &'static str; + fn noise(&mut self) -> f64; +} + +impl fmt::Display for dyn Ditherer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + +// 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. +// + +pub struct TriangularDitherer { + cached_rng: ThreadRng, + distribution: Triangular, +} + +impl Ditherer for TriangularDitherer { + fn new() -> Self { + Self { + cached_rng: rand::thread_rng(), + // 2 LSB peak-to-peak needed to linearize the response: + distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn noise(&mut self) -> f64 { + self.distribution.sample(&mut self.cached_rng) + } +} + +impl TriangularDitherer { + pub const NAME: &'static str = "tpdf"; +} + +pub struct GaussianDitherer { + cached_rng: ThreadRng, + distribution: Normal, +} + +impl Ditherer for GaussianDitherer { + fn new() -> Self { + Self { + cached_rng: rand::thread_rng(), + // 1/2 LSB RMS needed to linearize the response: + distribution: Normal::new(0.0, 0.5).unwrap(), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn noise(&mut self) -> f64 { + self.distribution.sample(&mut self.cached_rng) + } +} + +impl GaussianDitherer { + pub const NAME: &'static str = "gpdf"; +} + +pub struct HighPassDitherer { + active_channel: usize, + previous_noises: [f64; NUM_CHANNELS], + cached_rng: ThreadRng, + distribution: Uniform, +} + +impl Ditherer for HighPassDitherer { + fn new() -> Self { + Self { + active_channel: 0, + previous_noises: [0.0; NUM_CHANNELS], + cached_rng: rand::thread_rng(), + distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn noise(&mut self) -> f64 { + let new_noise = self.distribution.sample(&mut self.cached_rng); + let high_passed_noise = new_noise - self.previous_noises[self.active_channel]; + self.previous_noises[self.active_channel] = new_noise; + self.active_channel ^= 1; + high_passed_noise + } +} + +impl HighPassDitherer { + pub const NAME: &'static str = "tpdf_hp"; +} + +pub fn mk_ditherer() -> Box { + Box::new(D::new()) +} + +pub type DithererBuilder = fn() -> Box; + +pub fn find_ditherer(name: Option) -> Option { + match name.as_deref() { + Some(TriangularDitherer::NAME) => Some(mk_ditherer::), + Some(GaussianDitherer::NAME) => Some(mk_ditherer::), + Some(HighPassDitherer::NAME) => Some(mk_ditherer::), + _ => None, + } +} diff --git a/playback/src/lib.rs b/playback/src/lib.rs index 58423380..689b8470 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -9,5 +9,10 @@ pub mod audio_backend; pub mod config; mod convert; mod decoder; +pub mod dither; pub mod mixer; 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; diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 5e0a963f..8bee9e0d 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -1,218 +1,266 @@ -use super::AudioFilter; -use super::{Mixer, MixerConfig}; -use std::error::Error; +use crate::player::{db_to_ratio, ratio_to_db}; -const SND_CTL_TLV_DB_GAIN_MUTE: i64 = -9999999; +use super::mappings::{LogMapping, MappedCtrl, VolumeMapping}; +use super::{Mixer, MixerConfig, VolumeCtrl}; -#[derive(Clone)] -struct AlsaMixerVolumeParams { - min: i64, - max: i64, - range: f64, - min_db: alsa::mixer::MilliBel, - max_db: alsa::mixer::MilliBel, - has_switch: bool, -} +use alsa::ctl::{ElemId, ElemIface}; +use alsa::mixer::{MilliBel, SelemChannelId, SelemId}; +use alsa::{Ctl, Round}; + +use std::ffi::CString; #[derive(Clone)] pub struct AlsaMixer { config: MixerConfig, - params: AlsaMixerVolumeParams, + min: i64, + max: i64, + range: i64, + min_db: f64, + max_db: f64, + db_range: f64, + has_switch: bool, + is_softvol: bool, + use_linear_in_db: bool, } -impl AlsaMixer { - fn pvol(&self, vol: T, min: T, max: T) -> f64 - where - T: std::ops::Sub + Copy, - f64: std::convert::From<::Output>, - { - f64::from(vol - min) / f64::from(max - min) - } - - fn init_mixer(mut config: MixerConfig) -> Result> { - let mixer = alsa::mixer::Mixer::new(&config.card, false)?; - let sid = alsa::mixer::SelemId::new(&config.mixer, config.index); - - let selem = mixer.find_selem(&sid).unwrap_or_else(|| { - panic!( - "Couldn't find simple mixer control for {},{}", - &config.mixer, &config.index, - ) - }); - let (min, max) = selem.get_playback_volume_range(); - let (min_db, max_db) = selem.get_playback_db_range(); - let hw_mix = selem - .get_playback_vol_db(alsa::mixer::SelemChannelId::mono()) - .is_ok(); - let has_switch = selem.has_playback_switch(); - if min_db != alsa::mixer::MilliBel(SND_CTL_TLV_DB_GAIN_MUTE) { - warn!("Alsa min-db is not SND_CTL_TLV_DB_GAIN_MUTE!!"); - } - info!( - "Alsa Mixer info min: {} ({:?}[dB]) -- max: {} ({:?}[dB]) HW: {:?}", - min, min_db, max, max_db, hw_mix - ); - - if config.mapped_volume && (max_db - min_db <= alsa::mixer::MilliBel(24)) { - warn!( - "Switching to linear volume mapping, control range: {:?}", - max_db - min_db - ); - config.mapped_volume = false; - } else if !config.mapped_volume { - info!("Using Alsa linear volume"); - } - - if min_db != alsa::mixer::MilliBel(SND_CTL_TLV_DB_GAIN_MUTE) { - debug!("Alsa min-db is not SND_CTL_TLV_DB_GAIN_MUTE!!"); - } - - Ok(AlsaMixer { - config, - params: AlsaMixerVolumeParams { - min, - max, - range: (max - min) as f64, - min_db, - max_db, - has_switch, - }, - }) - } - - fn map_volume(&self, set_volume: Option) -> Result> { - let mixer = alsa::mixer::Mixer::new(&self.config.card, false)?; - let sid = alsa::mixer::SelemId::new(&*self.config.mixer, self.config.index); - - let selem = mixer.find_selem(&sid).unwrap(); - let cur_vol = selem - .get_playback_volume(alsa::mixer::SelemChannelId::mono()) - .expect("Couldn't get current volume"); - let cur_vol_db = selem - .get_playback_vol_db(alsa::mixer::SelemChannelId::mono()) - .unwrap_or(alsa::mixer::MilliBel(-SND_CTL_TLV_DB_GAIN_MUTE)); - - let mut new_vol: u16 = 0; - trace!("Current alsa volume: {}{:?}", cur_vol, cur_vol_db); - - match set_volume { - Some(vol) => { - if self.params.has_switch { - let is_muted = selem - .get_playback_switch(alsa::mixer::SelemChannelId::mono()) - .map(|b| b == 0) - .unwrap_or(false); - if vol == 0 { - debug!("Toggling mute::True"); - selem.set_playback_switch_all(0).expect("Can't switch mute"); - - return Ok(vol); - } else if is_muted { - debug!("Toggling mute::False"); - selem.set_playback_switch_all(1).expect("Can't reset mute"); - } - } - - if self.config.mapped_volume { - // Cubic mapping ala alsamixer - // https://linux.die.net/man/1/alsamixer - // In alsamixer, the volume is mapped to a value that is more natural for a - // human ear. The mapping is designed so that the position in the interval is - // proportional to the volume as a human ear would perceive it, i.e. the - // position is the cubic root of the linear sample multiplication factor. For - // controls with a small range (24 dB or less), the mapping is linear in the dB - // values so that each step has the same size visually. TODO - // TODO: Check if min is not mute! - let vol_db = (self.pvol(vol, 0x0000, 0xFFFF).log10() * 6000.0).floor() as i64 - + self.params.max_db.0; - selem - .set_playback_db_all(alsa::mixer::MilliBel(vol_db), alsa::Round::Floor) - .expect("Couldn't set alsa dB volume"); - debug!( - "Mapping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [dB] - {} [i64]", - self.pvol(vol, 0x0000, 0xFFFF) * 100.0, - vol, - self.pvol( - vol_db as f64, - self.params.min as f64, - self.params.max as f64 - ) * 100.0, - vol_db as f64 / 100.0, - vol_db - ); - } else { - // Linear mapping - let alsa_volume = - ((vol as f64 / 0xFFFF as f64) * self.params.range) as i64 + self.params.min; - selem - .set_playback_volume_all(alsa_volume) - .expect("Couldn't set alsa raw volume"); - debug!( - "Mapping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [i64]", - self.pvol(vol, 0x0000, 0xFFFF) * 100.0, - vol, - self.pvol( - alsa_volume as f64, - self.params.min as f64, - self.params.max as f64 - ) * 100.0, - alsa_volume - ); - }; - } - None => { - new_vol = (((cur_vol - self.params.min) as f64 / self.params.range) * 0xFFFF as f64) - as u16; - debug!( - "Mapping volume [{:.3}%] {:?} [u16] <<- Alsa [{:.3}%] {:?} [i64]", - self.pvol(new_vol, 0x0000, 0xFFFF), - new_vol, - self.pvol( - cur_vol as f64, - self.params.min as f64, - self.params.max as f64 - ), - cur_vol - ); - } - } - - Ok(new_vol) - } -} +// min_db cannot be depended on to be mute. Also note that contrary to +// its name copied verbatim from Alsa, this is in millibel scale. +const SND_CTL_TLV_DB_GAIN_MUTE: MilliBel = MilliBel(-9999999); +const ZERO_DB: MilliBel = MilliBel(0); impl Mixer for AlsaMixer { - fn open(config: Option) -> AlsaMixer { - let config = config.unwrap_or_default(); + fn open(config: MixerConfig) -> Self { info!( - "Setting up new mixer: card:{} mixer:{} index:{}", - config.card, config.mixer, config.index + "Mixing with alsa and volume control: {:?} for card: {} with mixer control: {},{}", + config.volume_ctrl, config.card, config.control, config.index, ); - AlsaMixer::init_mixer(config).expect("Error setting up mixer!") + + let mut config = config; // clone + + let mixer = + alsa::mixer::Mixer::new(&config.card, 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"); + + // Query capabilities + let has_switch = simple_element.has_playback_switch(); + let is_softvol = simple_element + .get_playback_vol_db(SelemChannelId::mono()) + .is_err(); + + // Query raw volume range + let (min, max) = simple_element.get_playback_volume_range(); + let range = i64::abs(max - min); + + // 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 mut element_id = ElemId::new(ElemIface::Mixer); + element_id.set_name( + &CString::new(config.control.as_str()) + .expect("Could not open Alsa softvol with that name"), + ); + element_id.set_index(config.index); + let (min_millibel, mut max_millibel) = control + .get_db_range(&element_id) + .expect("Could not get Alsa softvol dB range"); + + // Alsa can report incorrect maximum volumes due to rounding + // errors. e.g. Alsa rounds [-60.0..0.0] in range [0..255] to + // step size 0.23. Then multiplying 0.23 by 255 incorrectly + // returns a dB range of 58.65 instead of 60 dB, from + // [-60.00..-1.35]. This workaround checks the default case + // where the maximum dB volume is expected to be 0, and cannot + // cover all cases. + if max_millibel != ZERO_DB { + warn!("Alsa mixer reported maximum dB != 0, which is suspect"); + let reported_step_size = (max_millibel - min_millibel).0 / range; + let assumed_step_size = (ZERO_DB - min_millibel).0 / range; + if reported_step_size == assumed_step_size { + warn!("Alsa rounding error detected, setting maximum dB to {:.2} instead of {:.2}", ZERO_DB.to_db(), max_millibel.to_db()); + max_millibel = ZERO_DB; + } else { + warn!("Please manually set with `--volume-ctrl` if this is incorrect"); + } + } + (min_millibel, max_millibel) + } else { + let (mut min_millibel, max_millibel) = simple_element.get_playback_db_range(); + + // Some controls report that their minimum volume is mute, instead + // of their actual lowest dB setting before that. + if min_millibel == SND_CTL_TLV_DB_GAIN_MUTE && min < max { + debug!("Alsa mixer reported minimum dB as mute, trying workaround"); + min_millibel = simple_element + .ask_playback_vol_db(min + 1) + .expect("Could not convert Alsa raw volume to dB volume"); + } + (min_millibel, max_millibel) + }; + + let min_db = min_millibel.to_db() as f64; + let max_db = max_millibel.to_db() as f64; + let db_range = f64::abs(max_db - min_db); + + // Synchronize the volume control dB range with the mixer control, + // unless it was already set with a command line option. + if !config.volume_ctrl.range_ok() { + config.volume_ctrl.set_db_range(db_range); + } + + // For hardware controls with a small range (24 dB or less), + // force using the dB API with a linear mapping. + let mut use_linear_in_db = false; + if !is_softvol && db_range <= 24.0 { + use_linear_in_db = true; + config.volume_ctrl = VolumeCtrl::Linear; + } + + debug!("Alsa mixer control is softvol: {}", is_softvol); + debug!("Alsa support for playback (mute) switch: {}", has_switch); + debug!("Alsa raw volume range: [{}..{}] ({})", min, max, range); + debug!( + "Alsa dB volume range: [{:.2}..{:.2}] ({:.2})", + min_db, max_db, db_range + ); + debug!("Alsa forcing linear dB mapping: {}", use_linear_in_db); + + Self { + config, + min, + max, + range, + min_db, + max_db, + db_range, + has_switch, + is_softvol, + use_linear_in_db, + } } - fn start(&self) {} - - fn stop(&self) {} - fn volume(&self) -> u16 { - match self.map_volume(None) { - Ok(vol) => vol, - Err(e) => { - error!("Error getting volume for <{}>, {:?}", self.config.card, e); - 0 - } + let mixer = + alsa::mixer::Mixer::new(&self.config.card, 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"); + + if self.switched_off() { + return 0; } + + let mut mapped_volume = if self.is_softvol { + let raw_volume = simple_element + .get_playback_volume(SelemChannelId::mono()) + .expect("Could not get raw Alsa volume"); + raw_volume as f64 / self.range as f64 - self.min as f64 + } else { + let db_volume = simple_element + .get_playback_vol_db(SelemChannelId::mono()) + .expect("Could not get Alsa dB volume") + .to_db() as f64; + + if self.use_linear_in_db { + (db_volume - self.min_db) / self.db_range + } else if f64::abs(db_volume - SND_CTL_TLV_DB_GAIN_MUTE.to_db() as f64) <= f64::EPSILON + { + 0.0 + } else { + db_to_ratio(db_volume - self.max_db) + } + }; + + // see comment in `set_volume` why we are handling an antilog volume + if mapped_volume > 0.0 && self.is_some_linear() { + mapped_volume = LogMapping::linear_to_mapped(mapped_volume, self.db_range); + } + + self.config.volume_ctrl.from_mapped(mapped_volume) } fn set_volume(&self, volume: u16) { - match self.map_volume(Some(volume)) { - Ok(_) => (), - Err(e) => error!("Error setting volume for <{}>, {:?}", self.config.card, e), - } - } + let mixer = + alsa::mixer::Mixer::new(&self.config.card, 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"); - fn get_audio_filter(&self) -> Option> { - None + if self.has_switch { + if volume == 0 { + debug!("Disabling playback (setting mute) on Alsa"); + simple_element + .set_playback_switch_all(0) + .expect("Could not disable playback (set mute) on Alsa"); + } else if self.switched_off() { + debug!("Enabling playback (unsetting mute) on Alsa"); + simple_element + .set_playback_switch_all(1) + .expect("Could not enable playback (unset mute) on Alsa"); + } + } + + let mut mapped_volume = self.config.volume_ctrl.to_mapped(volume); + + // Alsa's linear algorithms map everything onto log. Alsa softvol does + // this internally. In the case of `use_linear_in_db` this happens + // automatically by virtue of the dB scale. This means that linear + // controls become log, log becomes log-on-log, and so on. To make + // the controls work as expected, perform an antilog calculation to + // counteract what Alsa will be doing to the set volume. + if mapped_volume > 0.0 && self.is_some_linear() { + mapped_volume = LogMapping::mapped_to_linear(mapped_volume, self.db_range); + } + + if self.is_softvol { + let scaled_volume = (self.min as f64 + mapped_volume * self.range as f64) as i64; + debug!("Setting Alsa raw volume to {}", scaled_volume); + simple_element + .set_playback_volume_all(scaled_volume) + .expect("Could not set Alsa raw volume"); + return; + } + + let db_volume = if self.use_linear_in_db { + self.min_db + mapped_volume * self.db_range + } else if volume == 0 { + // prevent ratio_to_db(0.0) from returning -inf + SND_CTL_TLV_DB_GAIN_MUTE.to_db() as f64 + } else { + ratio_to_db(mapped_volume) + self.max_db + }; + + debug!("Setting Alsa volume to {:.2} dB", db_volume); + simple_element + .set_playback_db_all(MilliBel::from_db(db_volume as f32), Round::Floor) + .expect("Could not set Alsa dB volume"); + } +} + +impl AlsaMixer { + pub const NAME: &'static str = "alsa"; + + fn switched_off(&self) -> bool { + if !self.has_switch { + return false; + } + + let mixer = + alsa::mixer::Mixer::new(&self.config.card, 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"); + + simple_element + .get_playback_switch(SelemChannelId::mono()) + .map(|playback| playback == 0) + .unwrap_or(false) + } + + fn is_some_linear(&self) -> bool { + self.is_softvol || self.use_linear_in_db } } diff --git a/playback/src/mixer/mappings.rs b/playback/src/mixer/mappings.rs new file mode 100644 index 00000000..04cef439 --- /dev/null +++ b/playback/src/mixer/mappings.rs @@ -0,0 +1,163 @@ +use super::VolumeCtrl; +use crate::player::db_to_ratio; + +pub trait MappedCtrl { + fn to_mapped(&self, volume: u16) -> f64; + fn from_mapped(&self, mapped_volume: f64) -> u16; + + fn db_range(&self) -> f64; + fn set_db_range(&mut self, new_db_range: f64); + fn range_ok(&self) -> bool; +} + +impl MappedCtrl for VolumeCtrl { + fn to_mapped(&self, volume: u16) -> f64 { + // More than just an optimization, this ensures that zero volume is + // really mute (both the log and cubic equations would otherwise not + // reach zero). + if volume == 0 { + return 0.0; + } else if volume == Self::MAX_VOLUME { + // And limit in case of rounding errors (as is the case for log). + return 1.0; + } + + let normalized_volume = volume as f64 / Self::MAX_VOLUME as f64; + let mapped_volume = if self.range_ok() { + match *self { + Self::Cubic(db_range) => { + CubicMapping::linear_to_mapped(normalized_volume, db_range) + } + Self::Log(db_range) => LogMapping::linear_to_mapped(normalized_volume, db_range), + _ => normalized_volume, + } + } else { + // Ensure not to return -inf or NaN due to division by zero. + error!( + "{:?} does not work with 0 dB range, using linear mapping instead", + self + ); + normalized_volume + }; + + debug!( + "Input volume {} mapped to: {:.2}%", + volume, + mapped_volume * 100.0 + ); + + mapped_volume + } + + fn from_mapped(&self, mapped_volume: f64) -> u16 { + // More than just an optimization, this ensures that zero mapped volume + // is unmapped to non-negative real numbers (otherwise the log and cubic + // equations would respectively return -inf and -1/9.) + if f64::abs(mapped_volume - 0.0) <= f64::EPSILON { + return 0; + } else if f64::abs(mapped_volume - 1.0) <= f64::EPSILON { + return Self::MAX_VOLUME; + } + + let unmapped_volume = if self.range_ok() { + match *self { + Self::Cubic(db_range) => CubicMapping::mapped_to_linear(mapped_volume, db_range), + Self::Log(db_range) => LogMapping::mapped_to_linear(mapped_volume, db_range), + _ => mapped_volume, + } + } else { + // Ensure not to return -inf or NaN due to division by zero. + error!( + "{:?} does not work with 0 dB range, using linear mapping instead", + self + ); + mapped_volume + }; + + (unmapped_volume * Self::MAX_VOLUME as f64) as u16 + } + + fn db_range(&self) -> f64 { + match *self { + Self::Fixed => 0.0, + Self::Linear => Self::DEFAULT_DB_RANGE, // arbitrary, could be anything > 0 + Self::Log(db_range) | Self::Cubic(db_range) => db_range, + } + } + + fn set_db_range(&mut self, new_db_range: f64) { + match self { + Self::Cubic(ref mut db_range) | Self::Log(ref mut db_range) => *db_range = new_db_range, + _ => error!("Invalid to set dB range for volume control type {:?}", self), + } + + debug!("Volume control is now {:?}", self) + } + + fn range_ok(&self) -> bool { + self.db_range() > 0.0 || matches!(self, Self::Fixed | Self::Linear) + } +} + +pub trait VolumeMapping { + fn linear_to_mapped(unmapped_volume: f64, db_range: f64) -> f64; + fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64; +} + +// Volume conversion taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html#ideal2 +// +// As the human auditory system has a logarithmic sensitivity curve, this +// mapping results in a near linear loudness experience with the listener. +pub struct LogMapping {} +impl VolumeMapping for LogMapping { + fn linear_to_mapped(normalized_volume: f64, db_range: f64) -> f64 { + let (db_ratio, ideal_factor) = Self::coefficients(db_range); + f64::exp(ideal_factor * normalized_volume) / db_ratio + } + + fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64 { + let (db_ratio, ideal_factor) = Self::coefficients(db_range); + f64::ln(db_ratio * mapped_volume) / ideal_factor + } +} + +impl LogMapping { + fn coefficients(db_range: f64) -> (f64, f64) { + let db_ratio = db_to_ratio(db_range); + let ideal_factor = f64::ln(db_ratio); + (db_ratio, ideal_factor) + } +} + +// Ported from: https://github.com/alsa-project/alsa-utils/blob/master/alsamixer/volume_mapping.c +// which in turn was inspired by: https://www.robotplanet.dk/audio/audio_gui_design/ +// +// Though this mapping is computationally less expensive than the logarithmic +// mapping, it really does not matter as librespot memoizes the mapped value. +// Use this mapping if you have some reason to mimic Alsa's native mixer or +// prefer a more granular control in the upper volume range. +// +// Note: https://www.dr-lex.be/info-stuff/volumecontrols.html#ideal3 shows +// better approximations to the logarithmic curve but because we only intend +// to mimic Alsa here, we do not implement them. If your desire is to use a +// logarithmic mapping, then use that volume control. +pub struct CubicMapping {} +impl VolumeMapping for CubicMapping { + fn linear_to_mapped(normalized_volume: f64, db_range: f64) -> f64 { + let min_norm = Self::min_norm(db_range); + f64::powi(normalized_volume * (1.0 - min_norm) + min_norm, 3) + } + + fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64 { + let min_norm = Self::min_norm(db_range); + (mapped_volume.powf(1.0 / 3.0) - min_norm) / (1.0 - min_norm) + } +} + +impl CubicMapping { + fn min_norm(db_range: f64) -> f64 { + // Note that this 60.0 is unrelated to DEFAULT_DB_RANGE. + // Instead, it's the cubic voltage to dB ratio. + f64::powf(10.0, -1.0 * db_range / 60.0) + } +} diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index af41c6f4..ed39582e 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -1,20 +1,28 @@ +use crate::config::VolumeCtrl; + +pub mod mappings; +use self::mappings::MappedCtrl; + pub trait Mixer: Send { - fn open(_: Option) -> Self + fn open(config: MixerConfig) -> Self where Self: Sized; - fn start(&self); - fn stop(&self); + fn set_volume(&self, volume: u16); fn volume(&self) -> u16; + fn get_audio_filter(&self) -> Option> { None } } pub trait AudioFilter { - fn modify_stream(&self, data: &mut [f32]); + fn modify_stream(&self, data: &mut [f64]); } +pub mod softmixer; +use self::softmixer::SoftMixer; + #[cfg(feature = "alsa-backend")] pub mod alsamixer; #[cfg(feature = "alsa-backend")] @@ -23,36 +31,33 @@ use self::alsamixer::AlsaMixer; #[derive(Debug, Clone)] pub struct MixerConfig { pub card: String, - pub mixer: String, + pub control: String, pub index: u32, - pub mapped_volume: bool, + pub volume_ctrl: VolumeCtrl, } impl Default for MixerConfig { fn default() -> MixerConfig { MixerConfig { card: String::from("default"), - mixer: String::from("PCM"), + control: String::from("PCM"), index: 0, - mapped_volume: true, + volume_ctrl: VolumeCtrl::default(), } } } -pub mod softmixer; -use self::softmixer::SoftMixer; +pub type MixerFn = fn(MixerConfig) -> Box; -type MixerFn = fn(Option) -> Box; - -fn mk_sink(device: Option) -> Box { - Box::new(M::open(device)) +fn mk_sink(config: MixerConfig) -> Box { + Box::new(M::open(config)) } -pub fn find>(name: Option) -> Option { - match name.as_ref().map(AsRef::as_ref) { - None | Some("softvol") => Some(mk_sink::), +pub fn find(name: Option<&str>) -> Option { + match name { + None | Some(SoftMixer::NAME) => Some(mk_sink::), #[cfg(feature = "alsa-backend")] - Some("alsa") => Some(mk_sink::), + Some(AlsaMixer::NAME) => Some(mk_sink::), _ => None, } } diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index ec8ed6b2..27448237 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -1,28 +1,40 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use super::AudioFilter; +use super::{MappedCtrl, VolumeCtrl}; use super::{Mixer, MixerConfig}; #[derive(Clone)] pub struct SoftMixer { - volume: Arc, + // There is no AtomicF64, so we store the f64 as bits in a u64 field. + // It's much faster than a Mutex. + volume: Arc, + volume_ctrl: VolumeCtrl, } impl Mixer for SoftMixer { - fn open(_: Option) -> SoftMixer { - SoftMixer { - volume: Arc::new(AtomicUsize::new(0xFFFF)), + fn open(config: MixerConfig) -> Self { + let volume_ctrl = config.volume_ctrl; + info!("Mixing with softvol and volume control: {:?}", volume_ctrl); + + Self { + volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))), + volume_ctrl, } } - fn start(&self) {} - fn stop(&self) {} + fn volume(&self) -> u16 { - self.volume.load(Ordering::Relaxed) as u16 + let mapped_volume = f64::from_bits(self.volume.load(Ordering::Relaxed)); + self.volume_ctrl.from_mapped(mapped_volume) } + fn set_volume(&self, volume: u16) { - self.volume.store(volume as usize, Ordering::Relaxed); + let mapped_volume = self.volume_ctrl.to_mapped(volume); + self.volume + .store(mapped_volume.to_bits(), Ordering::Relaxed) } + fn get_audio_filter(&self) -> Option> { Some(Box::new(SoftVolumeApplier { volume: self.volume.clone(), @@ -30,17 +42,20 @@ impl Mixer for SoftMixer { } } +impl SoftMixer { + pub const NAME: &'static str = "softmixer"; +} + struct SoftVolumeApplier { - volume: Arc, + volume: Arc, } impl AudioFilter for SoftVolumeApplier { - fn modify_stream(&self, data: &mut [f32]) { - let volume = self.volume.load(Ordering::Relaxed) as u16; - if volume != 0xFFFF { - let volume_factor = volume as f64 / 0xFFFF as f64; + fn modify_stream(&self, data: &mut [f64]) { + let volume = f64::from_bits(self.volume.load(Ordering::Relaxed)); + if volume < 1.0 { for x in data.iter_mut() { - *x = (*x as f64 * volume_factor) as f32; + *x *= volume; } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index d67d2f88..0249db9c 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -2,6 +2,7 @@ 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}; @@ -13,11 +14,12 @@ use tokio::sync::{mpsc, oneshot}; use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController}; use crate::audio::{ - READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, - READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, + READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, + READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, }; 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; @@ -25,12 +27,10 @@ use crate::decoder::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; -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; +use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND}; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; -const DB_VOLTAGE_RATIO: f32 = 20.0; +pub const DB_VOLTAGE_RATIO: f64 = 20.0; pub struct Player { commands: Option>, @@ -59,13 +59,14 @@ struct PlayerInternal { sink_event_callback: Option, audio_filter: Option>, event_senders: Vec>, + converter: Converter, limiter_active: bool, limiter_attack_counter: u32, limiter_release_counter: u32, - limiter_peak_sample: f32, - limiter_factor: f32, - limiter_strength: f32, + limiter_peak_sample: f64, + limiter_factor: f64, + limiter_strength: f64, } enum PlayerCommand { @@ -196,6 +197,14 @@ impl PlayerEvent { pub type PlayerEventChannel = mpsc::UnboundedReceiver; +pub fn db_to_ratio(db: f64) -> f64 { + f64::powf(10.0, db / DB_VOLTAGE_RATIO) +} + +pub fn ratio_to_db(ratio: f64) -> f64 { + ratio.log10() * DB_VOLTAGE_RATIO +} + #[derive(Clone, Copy, Debug)] pub struct NormalisationData { track_gain_db: f32, @@ -205,14 +214,6 @@ pub struct NormalisationData { } impl NormalisationData { - pub fn db_to_ratio(db: f32) -> f32 { - f32::powf(10.0, db / DB_VOLTAGE_RATIO) - } - - pub fn ratio_to_db(ratio: f32) -> f32 { - ratio.log10() * DB_VOLTAGE_RATIO - } - 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))?; @@ -232,7 +233,7 @@ impl NormalisationData { Ok(r) } - fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 { + fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f64 { if !config.normalisation { return 1.0; } @@ -242,12 +243,12 @@ impl NormalisationData { NormalisationType::Track => [data.track_gain_db, data.track_peak], }; - let normalisation_power = gain_db + config.normalisation_pregain; - let mut normalisation_factor = Self::db_to_ratio(normalisation_power); + let normalisation_power = gain_db as f64 + config.normalisation_pregain; + let mut normalisation_factor = db_to_ratio(normalisation_power); - if normalisation_factor * gain_peak > config.normalisation_threshold { - let limited_normalisation_factor = config.normalisation_threshold / gain_peak; - let limited_normalisation_power = Self::ratio_to_db(limited_normalisation_factor); + if normalisation_factor * gain_peak as f64 > config.normalisation_threshold { + let limited_normalisation_factor = config.normalisation_threshold / gain_peak as f64; + let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); if config.normalisation_method == NormalisationMethod::Basic { warn!("Limiting gain to {:.2} dB for the duration of this track to stay under normalisation threshold.", limited_normalisation_power); @@ -263,21 +264,9 @@ impl NormalisationData { } debug!("Normalisation Data: {:?}", data); - debug!("Normalisation Type: {:?}", config.normalisation_type); - debug!( - "Normalisation Threshold: {:.1}", - Self::ratio_to_db(config.normalisation_threshold) - ); - debug!("Normalisation Method: {:?}", config.normalisation_method); - debug!("Normalisation Factor: {}", normalisation_factor); + debug!("Normalisation Factor: {:.2}%", normalisation_factor * 100.0); - if config.normalisation_method == NormalisationMethod::Dynamic { - debug!("Normalisation Attack: {:?}", config.normalisation_attack); - debug!("Normalisation Release: {:?}", config.normalisation_release); - debug!("Normalisation Knee: {:?}", config.normalisation_knee); - } - - normalisation_factor + normalisation_factor as f64 } } @@ -294,9 +283,30 @@ impl Player { let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); let (event_sender, event_receiver) = mpsc::unbounded_channel(); + if config.normalisation { + debug!("Normalisation Type: {:?}", config.normalisation_type); + debug!( + "Normalisation Pregain: {:.1} dB", + config.normalisation_pregain + ); + debug!( + "Normalisation Threshold: {:.1} dBFS", + ratio_to_db(config.normalisation_threshold) + ); + debug!("Normalisation Method: {:?}", config.normalisation_method); + + if config.normalisation_method == NormalisationMethod::Dynamic { + debug!("Normalisation Attack: {:?}", config.normalisation_attack); + debug!("Normalisation Release: {:?}", config.normalisation_release); + debug!("Normalisation Knee: {:?}", config.normalisation_knee); + } + } + let handle = thread::spawn(move || { debug!("new Player[{}]", session.session_id()); + let converter = Converter::new(config.ditherer); + let internal = PlayerInternal { session, config, @@ -309,6 +319,7 @@ impl Player { sink_event_callback: None, audio_filter, event_senders: [event_sender].to_vec(), + converter, limiter_active: false, limiter_attack_counter: 0, @@ -412,7 +423,7 @@ impl Drop for Player { struct PlayerLoadedTrackData { decoder: Decoder, - normalisation_factor: f32, + normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -445,7 +456,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, - normalisation_factor: f32, + normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -456,7 +467,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, - normalisation_factor: f32, + normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -768,7 +779,7 @@ impl PlayerTrackLoader { } Err(_) => { warn!("Unable to extract normalisation data, using default value."); - 1.0_f32 + 1.0 } }; @@ -952,12 +963,12 @@ impl Future for PlayerInternal { 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. + // 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 > 1000 + lag > Duration::from_secs(1).as_millis() as i64 } }; if notify_about_position { @@ -1044,7 +1055,10 @@ impl PlayerInternal { } match self.sink.start() { Ok(()) => self.sink_status = SinkStatus::Running, - Err(err) => error!("Could not start audio: {}", err), + Err(err) => { + error!("Fatal error, could not start audio sink: {}", err); + exit(1); + } } } } @@ -1053,14 +1067,21 @@ impl PlayerInternal { match self.sink_status { SinkStatus::Running => { trace!("== Stopping sink =="); - self.sink.stop().unwrap(); - self.sink_status = if temporarily { - SinkStatus::TemporarilyClosed - } else { - SinkStatus::Closed - }; - if let Some(callback) = &mut self.sink_event_callback { - callback(self.sink_status); + match self.sink.stop() { + Ok(()) => { + self.sink_status = if temporarily { + SinkStatus::TemporarilyClosed + } else { + SinkStatus::Closed + }; + if let Some(callback) = &mut self.sink_event_callback { + callback(self.sink_status); + } + } + Err(err) => { + error!("Fatal error, could not stop audio sink: {}", err); + exit(1); + } } } SinkStatus::TemporarilyClosed => { @@ -1157,7 +1178,7 @@ impl PlayerInternal { } } - fn handle_packet(&mut self, packet: Option, normalisation_factor: f32) { + fn handle_packet(&mut self, packet: Option, normalisation_factor: f64) { match packet { Some(mut packet) => { if !packet.is_empty() { @@ -1167,7 +1188,7 @@ impl PlayerInternal { } if self.config.normalisation - && !(f32::abs(normalisation_factor - 1.0) <= f32::EPSILON + && !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON && self.config.normalisation_method == NormalisationMethod::Basic) { for sample in data.iter_mut() { @@ -1187,10 +1208,10 @@ impl PlayerInternal { { shaped_limiter_strength = 1.0 / (1.0 - + f32::powf( + + f64::powf( shaped_limiter_strength / (1.0 - shaped_limiter_strength), - -1.0 * self.config.normalisation_knee, + -self.config.normalisation_knee, )); } actual_normalisation_factor = @@ -1198,32 +1219,38 @@ impl PlayerInternal { + shaped_limiter_strength * self.limiter_factor; }; + // Cast the fields here for better readability + let normalisation_attack = + self.config.normalisation_attack.as_secs_f64(); + let normalisation_release = + self.config.normalisation_release.as_secs_f64(); + let limiter_release_counter = + self.limiter_release_counter as f64; + let limiter_attack_counter = self.limiter_attack_counter as f64; + let samples_per_second = SAMPLES_PER_SECOND as f64; + // Always check for peaks, even when the limiter is already active. // There may be even higher peaks than we initially targeted. // Check against the normalisation factor that would be applied normally. - let abs_sample = - ((*sample as f64 * normalisation_factor as f64) as f32) - .abs(); + let abs_sample = f64::abs(*sample * normalisation_factor); if abs_sample > self.config.normalisation_threshold { self.limiter_active = true; if self.limiter_release_counter > 0 { // A peak was encountered while releasing the limiter; // synchronize with the current release limiter strength. - self.limiter_attack_counter = (((SAMPLES_PER_SECOND - as f32 - * self.config.normalisation_release) - - self.limiter_release_counter as f32) - / (self.config.normalisation_release - / self.config.normalisation_attack)) + self.limiter_attack_counter = (((samples_per_second + * normalisation_release) + - limiter_release_counter) + / (normalisation_release / normalisation_attack)) as u32; self.limiter_release_counter = 0; } self.limiter_attack_counter = self.limiter_attack_counter.saturating_add(1); - self.limiter_strength = self.limiter_attack_counter as f32 - / (SAMPLES_PER_SECOND as f32 - * self.config.normalisation_attack); + + self.limiter_strength = limiter_attack_counter + / (samples_per_second * normalisation_attack); if abs_sample > self.limiter_peak_sample { self.limiter_peak_sample = abs_sample; @@ -1237,12 +1264,10 @@ impl PlayerInternal { // the limiter reached full strength. For that reason // start the release by synchronizing with the current // attack limiter strength. - self.limiter_release_counter = (((SAMPLES_PER_SECOND - as f32 - * self.config.normalisation_attack) - - self.limiter_attack_counter as f32) - * (self.config.normalisation_release - / self.config.normalisation_attack)) + self.limiter_release_counter = (((samples_per_second + * normalisation_attack) + - limiter_attack_counter) + * (normalisation_release / normalisation_attack)) as u32; self.limiter_attack_counter = 0; } @@ -1251,23 +1276,19 @@ impl PlayerInternal { self.limiter_release_counter.saturating_add(1); if self.limiter_release_counter - > (SAMPLES_PER_SECOND as f32 - * self.config.normalisation_release) - as u32 + > (samples_per_second * normalisation_release) as u32 { self.reset_limiter(); } else { - self.limiter_strength = ((SAMPLES_PER_SECOND as f32 - * self.config.normalisation_release) - - self.limiter_release_counter as f32) - / (SAMPLES_PER_SECOND as f32 - * self.config.normalisation_release); + self.limiter_strength = ((samples_per_second + * normalisation_release) + - limiter_release_counter) + / (samples_per_second * normalisation_release); } } } - *sample = - (*sample as f64 * actual_normalisation_factor as f64) as f32; + *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]. @@ -1280,9 +1301,9 @@ impl PlayerInternal { } } - if let Err(err) = self.sink.write(&packet) { - error!("Could not write audio: {}", err); - self.ensure_sink_stopped(false); + if let Err(err) = self.sink.write(&packet, &mut self.converter) { + error!("Fatal error, could not write audio to audio sink: {}", err); + exit(1); } } } @@ -1788,18 +1809,18 @@ impl PlayerInternal { // Request our read ahead range let request_data_length = max( (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS - * (0.001 * stream_loader_controller.ping_time_ms() as f64) - * bytes_per_second as f64) as usize, - (READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, + * stream_loader_controller.ping_time().as_secs_f32() + * bytes_per_second as f32) as usize, + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, ); stream_loader_controller.fetch_next(request_data_length); // Request the part we want to wait for blocking. This effecively means we wait for the previous request to partially complete. let wait_for_data_length = max( (READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS - * (0.001 * stream_loader_controller.ping_time_ms() as f64) - * bytes_per_second as f64) as usize, - (READ_AHEAD_BEFORE_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, + * stream_loader_controller.ping_time().as_secs_f32() + * 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); } diff --git a/src/lib.rs b/src/lib.rs index 7722e93e..75211282 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub use librespot_audio as audio; pub use librespot_connect as connect; pub use librespot_core as core; +pub use librespot_discovery as discovery; pub use librespot_metadata as metadata; pub use librespot_playback as playback; pub use librespot_protocol as protocol; diff --git a/src/main.rs b/src/main.rs index a5106af2..a3687aaa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,30 +9,31 @@ 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, VolumeCtrl}; +use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig}; use librespot::core::session::Session; use librespot::core::version; -use librespot::playback::audio_backend::{self, Sink, BACKENDS}; +use librespot::playback::audio_backend::{self, SinkBuilder, BACKENDS}; use librespot::playback::config::{ - AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, + AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, }; -use librespot::playback::mixer::{self, Mixer, MixerConfig}; -use librespot::playback::player::{NormalisationData, Player}; +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}; mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; -use std::convert::TryFrom; +use std::env; +use std::io::{stderr, Write}; use std::path::Path; +use std::pin::Pin; use std::process::exit; use std::str::FromStr; -use std::{env, time::Instant}; -use std::{ - io::{stderr, Write}, - pin::Pin, -}; - -const MILLIS: f32 = 1000.0; +use std::time::Duration; +use std::time::Instant; fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) @@ -66,7 +67,7 @@ fn setup_logging(verbose: bool) { } fn list_backends() { - println!("Available Backends : "); + println!("Available backends : "); for (&(name, _), idx) in BACKENDS.iter().zip(0..) { if idx == 0 { println!("- {} (default)", name); @@ -169,14 +170,11 @@ fn print_version() { ); } -#[derive(Clone)] struct Setup { format: AudioFormat, - backend: fn(Option, AudioFormat) -> Box, + backend: SinkBuilder, device: Option, - - mixer: fn(Option) -> Box, - + mixer: MixerFn, cache: Option, player_config: PlayerConfig, session_config: SessionConfig, @@ -190,182 +188,242 @@ struct Setup { } fn get_setup(args: &[String]) -> Setup { + const AP_PORT: &str = "ap-port"; + const AUTOPLAY: &str = "autoplay"; + const BACKEND: &str = "backend"; + const BITRATE: &str = "b"; + const CACHE: &str = "c"; + const CACHE_SIZE_LIMIT: &str = "cache-size-limit"; + const DEVICE: &str = "device"; + const DEVICE_TYPE: &str = "device-type"; + const DISABLE_AUDIO_CACHE: &str = "disable-audio-cache"; + const DISABLE_DISCOVERY: &str = "disable-discovery"; + const DISABLE_GAPLESS: &str = "disable-gapless"; + const DITHER: &str = "dither"; + 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 INITIAL_VOLUME: &str = "initial-volume"; + const MIXER_CARD: &str = "mixer-card"; + const MIXER_INDEX: &str = "mixer-index"; + const MIXER_NAME: &str = "mixer-name"; + const NAME: &str = "name"; + const NORMALISATION_ATTACK: &str = "normalisation-attack"; + const NORMALISATION_GAIN_TYPE: &str = "normalisation-gain-type"; + const NORMALISATION_KNEE: &str = "normalisation-knee"; + const NORMALISATION_METHOD: &str = "normalisation-method"; + const NORMALISATION_PREGAIN: &str = "normalisation-pregain"; + const NORMALISATION_RELEASE: &str = "normalisation-release"; + const NORMALISATION_THRESHOLD: &str = "normalisation-threshold"; + const ONEVENT: &str = "onevent"; + const PASSTHROUGH: &str = "passthrough"; + const PASSWORD: &str = "password"; + const PROXY: &str = "proxy"; + const SYSTEM_CACHE: &str = "system-cache"; + const USERNAME: &str = "username"; + const VERBOSE: &str = "verbose"; + const VERSION: &str = "version"; + const VOLUME_CTRL: &str = "volume-ctrl"; + const VOLUME_RANGE: &str = "volume-range"; + const ZEROCONF_PORT: &str = "zeroconf-port"; + let mut opts = getopts::Options::new(); - opts.optopt( - "c", + opts.optflag( + HELP, + "help", + "Print this help menu.", + ).optopt( + CACHE, "cache", "Path to a directory where files will be cached.", - "CACHE", + "PATH", ).optopt( "", - "system-cache", - "Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value", - "SYTEMCACHE", + SYSTEM_CACHE, + "Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value.", + "PATH", ).optopt( "", - "cache-size-limit", + CACHE_SIZE_LIMIT, "Limits the size of the cache for audio files.", - "CACHE_SIZE_LIMIT" - ).optflag("", "disable-audio-cache", "Disable caching of the audio data.") - .optopt("n", "name", "Device name", "NAME") - .optopt("", "device-type", "Displayed device type", "DEVICE_TYPE") - .optopt( - "b", - "bitrate", - "Bitrate (96, 160 or 320). Defaults to 160", - "BITRATE", - ) - .optopt( - "", - "onevent", - "Run PROGRAM when playback is about to begin.", - "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 to sign in with", "USERNAME") - .optopt("p", "password", "Password", "PASSWORD") - .optopt("", "proxy", "HTTP proxy to use when connecting", "PROXY") - .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", "AP_PORT") - .optflag("", "disable-discovery", "Disable discovery mode") - .optopt( - "", - "backend", - "Audio backend to use. Use '?' to list options", - "BACKEND", - ) - .optopt( - "", - "device", - "Audio device to use. Use '?' to list options if using portaudio or alsa", - "DEVICE", - ) - .optopt( - "", - "format", - "Output format (F32, S32, S24, S24_3 or S16). Defaults to S16", - "FORMAT", - ) - .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") - .optopt( - "m", - "mixer-name", - "Alsa mixer name, e.g \"PCM\" or \"Master\". Defaults to 'PCM'", - "MIXER_NAME", - ) - .optopt( - "", - "mixer-card", - "Alsa mixer card, e.g \"hw:0\" or similar from `aplay -l`. Defaults to 'default' ", - "MIXER_CARD", - ) - .optopt( - "", - "mixer-index", - "Alsa mixer index, Index of the cards mixer. Defaults to 0", - "MIXER_INDEX", - ) - .optflag( - "", - "mixer-linear-volume", - "Disable alsa's mapped volume scale (cubic). Default false", - ) - .optopt( - "", - "initial-volume", - "Initial volume in %, once connected (must be from 0 to 100)", - "VOLUME", - ) - .optopt( - "", - "zeroconf-port", - "The port the internal server advertised over zeroconf uses.", - "ZEROCONF_PORT", - ) - .optflag( - "", - "enable-volume-normalisation", - "Play all tracks at the same volume", - ) - .optopt( - "", - "normalisation-method", - "Specify the normalisation method to use - [basic, dynamic]. Default is dynamic.", - "NORMALISATION_METHOD", - ) - .optopt( - "", - "normalisation-gain-type", - "Specify the normalisation gain type to use - [track, album]. Default is album.", - "GAIN_TYPE", - ) - .optopt( - "", - "normalisation-pregain", - "Pregain (dB) applied by volume normalisation", - "PREGAIN", - ) - .optopt( - "", - "normalisation-threshold", - "Threshold (dBFS) to prevent clipping. Default is -1.0.", - "THRESHOLD", - ) - .optopt( - "", - "normalisation-attack", - "Attack time (ms) in which the dynamic limiter is reducing gain. Default is 5.", - "ATTACK", - ) - .optopt( - "", - "normalisation-release", - "Release or decay time (ms) in which the dynamic limiter is restoring gain. Default is 100.", - "RELEASE", - ) - .optopt( - "", - "normalisation-knee", - "Knee steepness of the dynamic limiter. Default is 1.0.", - "KNEE", - ) - .optopt( - "", - "volume-ctrl", - "Volume control type - [linear, log, fixed]. Default is logarithmic", - "VOLUME_CTRL" - ) - .optflag( - "", - "autoplay", - "autoplay similar songs when your music ends.", - ) - .optflag( - "", - "disable-gapless", - "disable gapless playback.", - ) - .optflag( - "", - "passthrough", - "Pass raw stream to output, only works for \"pipe\"." - ); + "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( + 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 to sign in with.", "USERNAME") + .optopt("p", PASSWORD, "Password", "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( + "", + 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.", + "NAME", + ) + .optopt( + "", + FORMAT, + "Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.", + "FORMAT", + ) + .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.", + "DITHER", + ) + .optopt("", "mixer", "Mixer to use {alsa|softvol}.", "MIXER") + .optopt( + "m", + MIXER_NAME, + "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", + ) + .optopt( + "", + MIXER_INDEX, + "Alsa index of the cards mixer. Defaults to 0.", + "INDEX", + ) + .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 advertised over zeroconf uses.", + "PORT", + ) + .optflag( + "", + ENABLE_VOLUME_NORMALISATION, + "Play all tracks at the same 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}. Defaults to album.", + "TYPE", + ) + .optopt( + "", + NORMALISATION_PREGAIN, + "Pregain (dB) applied by volume normalisation. Defaults to 0.", + "PREGAIN", + ) + .optopt( + "", + NORMALISATION_THRESHOLD, + "Threshold (dBFS) to prevent clipping. Defaults to -1.0.", + "THRESHOLD", + ) + .optopt( + "", + NORMALISATION_ATTACK, + "Attack time (ms) in which the dynamic limiter is reducing gain. Defaults to 5.", + "TIME", + ) + .optopt( + "", + NORMALISATION_RELEASE, + "Release or decay time (ms) in which the dynamic limiter is restoring gain. Defaults to 100.", + "TIME", + ) + .optopt( + "", + NORMALISATION_KNEE, + "Knee steepness of the dynamic limiter. Defaults to 1.0.", + "KNEE", + ) + .optopt( + "", + VOLUME_CTRL, + "Volume control type {cubic|fixed|linear|log}. Defaults to log.", + "VOLUME_CTRL" + ) + .optopt( + "", + VOLUME_RANGE, + "Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports.", + "RANGE", + ) + .optflag( + "", + AUTOPLAY, + "Automatically play similar songs when your music ends.", + ) + .optflag( + "", + DISABLE_GAPLESS, + "Disable gapless playback.", + ) + .optflag( + "", + PASSTHROUGH, + "Pass raw stream to output, only works for pipe and subprocess.", + ); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => { - eprintln!("error: {}\n{}", f.to_string(), usage(&args[0], &opts)); + eprintln!( + "Error parsing command line options: {}\n{}", + f, + usage(&args[0], &opts) + ); exit(1); } }; - if matches.opt_present("version") { + if matches.opt_present(HELP) { + println!("{}", usage(&args[0], &opts)); + exit(0); + } + + if matches.opt_present(VERSION) { print_version(); exit(0); } - let verbose = matches.opt_present("verbose"); + let verbose = matches.opt_present(VERBOSE); setup_logging(verbose); info!( @@ -376,7 +434,7 @@ fn get_setup(args: &[String]) -> Setup { build_id = version::BUILD_ID ); - let backend_name = matches.opt_str("backend"); + let backend_name = matches.opt_str(BACKEND); if backend_name == Some("?".into()) { list_backends(); exit(0); @@ -385,57 +443,95 @@ fn get_setup(args: &[String]) -> Setup { let backend = audio_backend::find(backend_name).expect("Invalid backend"); let format = matches - .opt_str("format") - .as_ref() - .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) + .opt_str(FORMAT) + .as_deref() + .map(|format| AudioFormat::from_str(format).expect("Invalid output format")) .unwrap_or_default(); - let device = matches.opt_str("device"); + let device = matches.opt_str(DEVICE); if device == Some("?".into()) { backend(device, format); exit(0); } - let mixer_name = matches.opt_str("mixer"); - let mixer = mixer::find(mixer_name.as_ref()).expect("Invalid mixer"); + let mixer_name = matches.opt_str(MIXER_NAME); + let mixer = mixer::find(mixer_name.as_deref()).expect("Invalid mixer"); - let mixer_config = MixerConfig { - card: matches - .opt_str("mixer-card") - .unwrap_or_else(|| String::from("default")), - mixer: matches - .opt_str("mixer-name") - .unwrap_or_else(|| String::from("PCM")), - index: matches - .opt_str("mixer-index") + 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 index = matches + .opt_str(MIXER_INDEX) .map(|index| index.parse::().unwrap()) - .unwrap_or(0), - mapped_volume: !matches.opt_present("mixer-linear-volume"), + .unwrap_or(0); + let control = matches + .opt_str(MIXER_NAME) + .unwrap_or_else(|| MixerConfig::default().control); + let mut volume_range = matches + .opt_str(VOLUME_RANGE) + .map(|range| range.parse::().unwrap()) + .unwrap_or_else(|| match mixer_name.as_deref() { + #[cfg(feature = "alsa-backend")] + 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") + }) + .unwrap_or_else(|| { + let mut volume_ctrl = VolumeCtrl::default(); + volume_ctrl.set_db_range(volume_range); + volume_ctrl + }); + + MixerConfig { + card, + control, + index, + volume_ctrl, + } }; let cache = { let audio_dir; let system_dir; - if matches.opt_present("disable-audio-cache") { + if matches.opt_present(DISABLE_AUDIO_CACHE) { audio_dir = None; system_dir = matches - .opt_str("system-cache") - .or_else(|| matches.opt_str("c")) + .opt_str(SYSTEM_CACHE) + .or_else(|| matches.opt_str(CACHE)) .map(|p| p.into()); } else { - let cache_dir = matches.opt_str("c"); + let cache_dir = matches.opt_str(CACHE); audio_dir = cache_dir .as_ref() .map(|p| AsRef::::as_ref(p).join("files")); system_dir = matches - .opt_str("system-cache") + .opt_str(SYSTEM_CACHE) .or(cache_dir) .map(|p| p.into()); } 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| { @@ -458,24 +554,28 @@ fn get_setup(args: &[String]) -> Setup { }; let initial_volume = matches - .opt_str("initial-volume") - .map(|volume| { - let volume = volume.parse::().unwrap(); + .opt_str(INITIAL_VOLUME) + .map(|initial_volume| { + let volume = initial_volume.parse::().unwrap(); if volume > 100 { - panic!("Initial volume must be in the range 0-100"); + error!("Initial volume must be in the range 0-100."); + // the cast will saturate, not necessary to take further action } - (volume as i32 * 0xFFFF / 100) as u16 + (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 }) - .or_else(|| cache.as_ref().and_then(Cache::volume)) - .unwrap_or(0x8000); + .or_else(|| match mixer_name.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") + .opt_str(ZEROCONF_PORT) .map(|port| port.parse::().unwrap()) .unwrap_or(0); let name = matches - .opt_str("name") + .opt_str(NAME) .unwrap_or_else(|| "Librespot".to_string()); let credentials = { @@ -488,8 +588,8 @@ fn get_setup(args: &[String]) -> Setup { }; get_credentials( - matches.opt_str("username"), - matches.opt_str("password"), + matches.opt_str(USERNAME), + matches.opt_str(PASSWORD), cached_credentials, password, ) @@ -501,12 +601,12 @@ 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: matches.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() { - panic!("Invalid proxy url, only urls on the format \"http://host:port\" are allowed"); + panic!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); } if url.scheme() != "http" { @@ -514,123 +614,154 @@ fn get_setup(args: &[String]) -> Setup { } url }, - Err(err) => panic!("Invalid proxy url: {}, only urls on the format \"http://host:port\" are allowed", err) + Err(err) => panic!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", err) } }, ), ap_port: matches - .opt_str("ap-port") + .opt_str(AP_PORT) .map(|port| port.parse::().expect("Invalid port")), } }; - let passthrough = matches.opt_present("passthrough"); - let player_config = { let bitrate = matches - .opt_str("b") - .as_ref() + .opt_str(BITRATE) + .as_deref() .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) .unwrap_or_default(); - let gain_type = matches - .opt_str("normalisation-gain-type") - .as_ref() + + 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_method = matches - .opt_str("normalisation-method") - .as_ref() - .map(|gain_type| { - NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method") + 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_default(); + .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 ditherer_name = matches.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 format == AudioFormat::F64 || format == AudioFormat::F32 { + unimplemented!("Dithering is not available on format {:?}", format); + } + Some(dither::find_ditherer(ditherer_name).expect("Invalid ditherer")) + } + // nothing set on command line => use default + None => match format { + AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { + PlayerConfig::default().ditherer + } + _ => None, + }, + }; + + let passthrough = matches.opt_present(PASSTHROUGH); PlayerConfig { bitrate, - gapless: !matches.opt_present("disable-gapless"), - normalisation: matches.opt_present("enable-volume-normalisation"), - normalisation_method, - normalisation_type: gain_type, - normalisation_pregain: matches - .opt_str("normalisation-pregain") - .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) - .unwrap_or(PlayerConfig::default().normalisation_pregain), - normalisation_threshold: matches - .opt_str("normalisation-threshold") - .map(|threshold| { - NormalisationData::db_to_ratio( - threshold - .parse::() - .expect("Invalid threshold float value"), - ) - }) - .unwrap_or(PlayerConfig::default().normalisation_threshold), - normalisation_attack: matches - .opt_str("normalisation-attack") - .map(|attack| attack.parse::().expect("Invalid attack float value") / MILLIS) - .unwrap_or(PlayerConfig::default().normalisation_attack), - normalisation_release: matches - .opt_str("normalisation-release") - .map(|release| { - release.parse::().expect("Invalid release float value") / MILLIS - }) - .unwrap_or(PlayerConfig::default().normalisation_release), - normalisation_knee: matches - .opt_str("normalisation-knee") - .map(|knee| knee.parse::().expect("Invalid knee float value")) - .unwrap_or(PlayerConfig::default().normalisation_knee), + gapless, passthrough, + normalisation, + normalisation_type, + normalisation_method, + normalisation_pregain, + normalisation_threshold, + normalisation_attack, + normalisation_release, + normalisation_knee, + ditherer, } }; let connect_config = { let device_type = matches - .opt_str("device-type") - .as_ref() + .opt_str(DEVICE_TYPE) + .as_deref() .map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type")) .unwrap_or_default(); - - let volume_ctrl = matches - .opt_str("volume-ctrl") - .as_ref() - .map(|volume_ctrl| VolumeCtrl::from_str(volume_ctrl).expect("Invalid volume ctrl 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, - volume: initial_volume, - volume_ctrl, - autoplay: matches.opt_present("autoplay"), + initial_volume, + has_volume_ctrl, + autoplay, } }; - let enable_discovery = !matches.opt_present("disable-discovery"); + 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); Setup { format, backend, - cache, - session_config, - player_config, - connect_config, - credentials, device, + mixer, + cache, + player_config, + session_config, + connect_config, + mixer_config, + credentials, enable_discovery, zeroconf_port, - mixer, - mixer_config, - player_event_program: matches.opt_str("onevent"), - emit_sink_events: matches.opt_present("emit-sink-events"), + player_event_program, + emit_sink_events, } } #[tokio::main(flavor = "current_thread")] async fn main() { - if env::var("RUST_BACKTRACE").is_err() { - env::set_var("RUST_BACKTRACE", "full") + const RUST_BACKTRACE: &str = "RUST_BACKTRACE"; + if env::var(RUST_BACKTRACE).is_err() { + env::set_var(RUST_BACKTRACE, "full") } let args: Vec = std::env::args().collect(); @@ -645,11 +776,14 @@ async fn main() { let mut connecting: Pin>> = Box::pin(future::pending()); if setup.enable_discovery { - let config = setup.connect_config.clone(); let device_id = setup.session_config.device_id.clone(); discovery = Some( - librespot_connect::discovery::discovery(config, device_id, setup.zeroconf_port) + librespot::discovery::Discovery::builder(device_id) + .name(setup.connect_config.name.clone()) + .device_type(setup.connect_config.device_type) + .port(setup.zeroconf_port) + .launch() .unwrap(), ); } @@ -697,7 +831,7 @@ async fn main() { session = &mut connecting, if !connecting.is_terminated() => match session { Ok(session) => { let mixer_config = setup.mixer_config.clone(); - let mixer = (setup.mixer)(Some(mixer_config)); + let mixer = (setup.mixer)(mixer_config); let player_config = setup.player_config.clone(); let connect_config = setup.connect_config.clone(); @@ -717,14 +851,14 @@ async fn main() { Ok(e) if e.success() => (), Ok(e) => { if let Some(code) = e.code() { - warn!("Sink event prog returned exit code {}", code); + warn!("Sink event program returned exit code {}", code); } else { - warn!("Sink event prog returned failure"); + warn!("Sink event program returned failure"); } - } + }, Err(e) => { warn!("Emitting sink event failed: {}", e); - } + }, } }))); } @@ -774,13 +908,21 @@ async fn main() { tokio::spawn(async move { match child.wait().await { - Ok(status) if !status.success() => error!("child exited with status {:?}", status.code()), - Err(e) => error!("failed to wait on child process: {}", e), - _ => {} + Ok(e) if e.success() => (), + Ok(e) => { + if let Some(code) = e.code() { + warn!("On event program returned exit code {}", code); + } else { + warn!("On event program returned failure"); + } + }, + Err(e) => { + warn!("On event program failed: {}", e); + }, } }); } else { - error!("program failed to start"); + warn!("On event program failed to start"); } } } From a21b3c9f86ffb1eaff97bd3076aae468519bbf72 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 28 Jun 2021 21:34:59 +0200 Subject: [PATCH 020/147] Revert "Lay groundwork for new Spotify API client (#805)" This reverts commit 39bf40bcc7b9712c5f952689fee8d877a4bf6cf8. --- .github/workflows/test.yml | 14 +- CHANGELOG.md | 38 +- Cargo.lock | 280 ++++---- Cargo.toml | 12 +- README.md | 1 - audio/src/fetch/mod.rs | 136 ++-- audio/src/fetch/receive.rs | 73 +- audio/src/lib.rs | 4 +- connect/Cargo.toml | 21 +- connect/src/discovery.rs | 264 ++++++- connect/src/lib.rs | 4 - connect/src/spirc.rs | 97 ++- contrib/librespot.service | 7 +- contrib/librespot.user.service | 12 - core/Cargo.toml | 2 - core/src/apresolve.rs | 243 ++++--- core/src/audio_key.rs | 13 +- core/src/channel.rs | 18 +- core/src/config.rs | 87 ++- core/src/connection/mod.rs | 13 +- core/src/http_client.rs | 34 - core/src/keymaster.rs | 26 + core/src/lib.rs | 8 +- core/src/mercury/mod.rs | 24 +- core/src/mercury/types.rs | 12 +- core/src/packet.rs | 41 -- core/src/session.rs | 129 ++-- core/src/spclient.rs | 1 - core/src/spotify_id.rs | 33 +- core/src/token.rs | 131 ---- discovery/Cargo.toml | 40 -- discovery/examples/discovery.rs | 25 - discovery/src/lib.rs | 150 ---- discovery/src/server.rs | 236 ------- examples/get_token.rs | 9 +- metadata/src/cover.rs | 3 +- playback/Cargo.toml | 19 +- playback/src/audio_backend/alsa.rs | 259 ++----- playback/src/audio_backend/gstreamer.rs | 20 +- playback/src/audio_backend/jackaudio.rs | 16 +- playback/src/audio_backend/mod.rs | 62 +- playback/src/audio_backend/pipe.rs | 52 +- playback/src/audio_backend/portaudio.rs | 30 +- playback/src/audio_backend/pulseaudio.rs | 22 +- playback/src/audio_backend/rodio.rs | 27 +- playback/src/audio_backend/sdl.rs | 22 +- playback/src/audio_backend/subprocess.rs | 5 - playback/src/config.rs | 95 +-- playback/src/convert.rs | 153 ++--- playback/src/decoder/lewton_decoder.rs | 11 +- playback/src/decoder/libvorbis_decoder.rs | 89 +++ playback/src/decoder/mod.rs | 22 +- playback/src/decoder/passthrough_decoder.rs | 9 +- playback/src/dither.rs | 150 ---- playback/src/lib.rs | 5 - playback/src/mixer/alsamixer.rs | 450 ++++++------ playback/src/mixer/mappings.rs | 163 ----- playback/src/mixer/mod.rs | 41 +- playback/src/mixer/softmixer.rs | 45 +- playback/src/player.rs | 207 +++--- src/lib.rs | 1 - src/main.rs | 722 ++++++++------------ 62 files changed, 1837 insertions(+), 3101 deletions(-) delete mode 100644 contrib/librespot.user.service delete mode 100644 core/src/http_client.rs create mode 100644 core/src/keymaster.rs delete mode 100644 core/src/packet.rs delete mode 100644 core/src/spclient.rs delete mode 100644 core/src/token.rs delete mode 100644 discovery/Cargo.toml delete mode 100644 discovery/examples/discovery.rs delete mode 100644 discovery/src/lib.rs delete mode 100644 discovery/src/server.rs create mode 100644 playback/src/decoder/libvorbis_decoder.rs delete mode 100644 playback/src/dither.rs delete mode 100644 playback/src/mixer/mappings.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e447ff9..825fc936 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,11 +11,6 @@ on: "Cargo.lock", "rustfmt.toml", ".github/workflows/*", - "!*.md", - "!contrib/*", - "!docs/*", - "!LICENSE", - "!*.sh", ] pull_request: paths: @@ -25,11 +20,6 @@ on: "Cargo.lock", "rustfmt.toml", ".github/workflows/*", - "!*.md", - "!contrib/*", - "!docs/*", - "!LICENSE", - "!*.sh", ] schedule: # Run CI every week @@ -109,8 +99,8 @@ jobs: - 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 build -p librespot-connect + - run: cargo build -p librespot-connect --no-default-features --features with-dns-sd - run: cargo hack build --each-feature test-windows: diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb63541..9a775d4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,44 +6,14 @@ 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] -### 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) - -### 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] 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 -- [playback] Store and pass samples in 64-bit floating point -- [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`: 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) - -### Deprecated -- [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate ### 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 + +* [librespot-audio] Removed `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot_audio`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. ### Fixed -- [connect] Fix step size on volume up/down events -- [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 -- [playback] `alsamixer`: make `cubic` consistent between cards that report minimum volume as mute, and cards that report some dB value -- [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 + +* [librespot-playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream ## [0.2.0] - 2021-05-04 diff --git a/Cargo.lock b/Cargo.lock index 37cbae56..1f97d578 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" @@ -168,9 +170,9 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" -version = "1.0.68" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" dependencies = [ "jobserver", ] @@ -235,17 +237,6 @@ dependencies = [ "libloading 0.7.0", ] -[[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.5.2" @@ -309,9 +300,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.1.4" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" +checksum = "dec1028182c380cc45a2e2c5ec841134f2dfd0f8f5f0a5bcd68004f81b5efdf4" dependencies = [ "libc", ] @@ -437,9 +428,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.15" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" +checksum = "a9d5813545e459ad3ca1bff9915e9ad7f1a47dc6a91b627ce321d5863b7dd253" dependencies = [ "futures-channel", "futures-core", @@ -529,6 +520,12 @@ dependencies = [ "slab", ] +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "generic-array" version = "0.14.4" @@ -550,9 +547,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" dependencies = [ "cfg-if 1.0.0", "libc", @@ -638,7 +635,7 @@ dependencies = [ "gstreamer-sys", "libc", "muldiv", - "num-rational 0.3.2", + "num-rational", "once_cell", "paste", "pretty-hex", @@ -819,15 +816,15 @@ dependencies = [ [[package]] name = "httparse" -version = "1.4.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" +checksum = "4a1ce40d6fc9764887c2fdc7305c3dcc429ba11ff981c1509416afd5697e4437" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "05842d0d43232b23ccb7060ecb0f0626922c21f30012e97b767b30afd4a5d4b9" [[package]] name = "humantime" @@ -837,9 +834,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.8" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3f71a7eea53a3f8257a7b4795373ff886397178cd634430ea94e12d7fe4fe34" +checksum = "1e5f105c494081baa3bf9e200b279e27ec1623895cd504c7dbef8d0b080fcf54" dependencies = [ "bytes", "futures-channel", @@ -1052,9 +1049,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.95" +version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" +checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "libloading" @@ -1076,12 +1073,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "libm" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" - [[package]] name = "libmdns" version = "0.6.1" @@ -1161,7 +1152,6 @@ dependencies = [ "librespot-audio", "librespot-connect", "librespot-core", - "librespot-discovery", "librespot-metadata", "librespot-playback", "librespot-protocol", @@ -1191,10 +1181,16 @@ dependencies = [ name = "librespot-connect" version = "0.2.0" dependencies = [ + "aes-ctr", + "base64", + "dns-sd", "form_urlencoded", + "futures-core", "futures-util", + "hmac", + "hyper", + "libmdns", "librespot-core", - "librespot-discovery", "librespot-playback", "librespot-protocol", "log", @@ -1202,8 +1198,10 @@ dependencies = [ "rand", "serde", "serde_json", + "sha-1", "tokio", "tokio-stream", + "url", ] [[package]] @@ -1225,9 +1223,7 @@ dependencies = [ "hyper-proxy", "librespot-protocol", "log", - "num", "num-bigint", - "num-derive", "num-integer", "num-traits", "once_cell", @@ -1249,31 +1245,6 @@ dependencies = [ "vergen", ] -[[package]] -name = "librespot-discovery" -version = "0.2.0" -dependencies = [ - "aes-ctr", - "base64", - "cfg-if 1.0.0", - "dns-sd", - "form_urlencoded", - "futures", - "futures-core", - "hex", - "hmac", - "hyper", - "libmdns", - "librespot-core", - "log", - "rand", - "serde_json", - "sha-1", - "simple_logger", - "thiserror", - "tokio", -] - [[package]] name = "librespot-metadata" version = "0.2.0" @@ -1292,6 +1263,7 @@ version = "0.2.0" dependencies = [ "alsa", "byteorder", + "cfg-if 1.0.0", "cpal", "futures-executor", "futures-util", @@ -1305,16 +1277,16 @@ dependencies = [ "librespot-audio", "librespot-core", "librespot-metadata", + "librespot-tremor", "log", "ogg", "portaudio-rs", - "rand", - "rand_distr", "rodio", "sdl2", "shell-words", "thiserror", "tokio", + "vorbis", "zerocopy", ] @@ -1327,6 +1299,18 @@ dependencies = [ "protobuf-codegen-pure", ] +[[package]] +name = "librespot-tremor" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97f525bff915d478a76940a7b988e5ea34911ba7280c97bd3a7673f54d68b4fe" +dependencies = [ + "cc", + "libc", + "ogg-sys", + "pkg-config", +] + [[package]] name = "lock_api" version = "0.4.4" @@ -1491,20 +1475,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational 0.4.0", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.0" @@ -1517,15 +1487,6 @@ dependencies = [ "rand", ] -[[package]] -name = "num-complex" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" -dependencies = [ - "num-traits", -] - [[package]] name = "num-derive" version = "0.3.3" @@ -1547,17 +1508,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-rational" version = "0.3.2" @@ -1569,18 +1519,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" -dependencies = [ - "autocfg", - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.14" @@ -1588,7 +1526,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -1625,9 +1562,9 @@ dependencies = [ [[package]] name = "oboe" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa187b38ae20374617b7ad418034ed3dc90ac980181d211518bd03537ae8f8d" +checksum = "4cfb2390bddb9546c0f7448fd1d2abdd39e6075206f960991eb28c7fa7f126c4" dependencies = [ "jni", "ndk", @@ -1639,9 +1576,9 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.4.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88e64835aa3f579c08d182526dc34e3907343d5b97e87b71a40ba5bca7aca9e" +checksum = "fe069264d082fc820dfa172f79be3f2e088ecfece9b1c47b0c9fd838d2bef103" dependencies = [ "cc", ] @@ -1655,6 +1592,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "ogg-sys" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a95b8c172e17df1a41bf8d666301d3b2c4efeb90d9d0415e2a4dc0668b35fdb2" +dependencies = [ + "gcc", + "libc", + "pkg-config", +] + [[package]] name = "once_cell" version = "1.7.2" @@ -1857,9 +1805,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" dependencies = [ "unicode-xid", ] @@ -1929,16 +1877,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_distr" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9e8f32ad24fb80d07d2323a9a2ce8b30d68a62b8cb4df88119ff49a698f038" -dependencies = [ - "num-traits", - "rand", -] - [[package]] name = "rand_hc" version = "0.3.0" @@ -2119,18 +2057,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.126" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" dependencies = [ "proc-macro2", "quote", @@ -2191,19 +2129,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simple_logger" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd57f17c093ead1d4a1499dc9acaafdd71240908d64775465543b8d9a9f1d198" -dependencies = [ - "atty", - "chrono", - "colored", - "log", - "winapi", -] - [[package]] name = "slab" version = "0.4.3" @@ -2331,18 +2256,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.25" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.25" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" dependencies = [ "proc-macro2", "quote", @@ -2376,9 +2301,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.6.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37" +checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5" dependencies = [ "autocfg", "bytes", @@ -2395,9 +2320,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.2.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" dependencies = [ "proc-macro2", "quote", @@ -2417,9 +2342,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.6" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" +checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0" dependencies = [ "futures-core", "pin-project-lite", @@ -2445,9 +2370,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.7" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +checksum = "940a12c99365c31ea8dd9ba04ec1be183ffe4920102bb7122c2f515437601e8e" dependencies = [ "bytes", "futures-core", @@ -2625,6 +2550,43 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "vorbis" +version = "0.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e8a194457075360557b82dac78f7ca2d65bbb6679bccfabae5f7c8c706cc776" +dependencies = [ + "libc", + "ogg-sys", + "vorbis-sys", + "vorbisfile-sys", +] + +[[package]] +name = "vorbis-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9ed6ef5361a85e68ccc005961d995c2d44e31f0816f142025f2ca2383dfbfd" +dependencies = [ + "cc", + "libc", + "ogg-sys", + "pkg-config", +] + +[[package]] +name = "vorbisfile-sys" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4306d7e1ac4699b55e20de9483750b90c250913188efd7484db6bfbe9042d1" +dependencies = [ + "gcc", + "libc", + "ogg-sys", + "pkg-config", + "vorbis-sys", +] + [[package]] name = "walkdir" version = "2.3.2" @@ -2708,9 +2670,9 @@ checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" [[package]] name = "web-sys" -version = "0.3.51" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index ced7d0f9..5df27872 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,10 +32,6 @@ version = "0.2.0" path = "core" version = "0.2.0" -[dependencies.librespot-discovery] -path = "discovery" -version = "0.2.0" - [dependencies.librespot-metadata] path = "metadata" version = "0.2.0" @@ -72,7 +68,10 @@ 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-tremor = ["librespot-playback/with-tremor"] +with-vorbis = ["librespot-playback/with-vorbis"] + +with-dns-sd = ["librespot-connect/with-dns-sd"] default = ["rodio-backend"] @@ -90,6 +89,5 @@ section = "sound" priority = "optional" assets = [ ["target/release/librespot", "usr/bin/", "755"], - ["contrib/librespot.service", "lib/systemd/system/", "644"], - ["contrib/librespot.user.service", "lib/systemd/user/", "644"] + ["contrib/librespot.service", "lib/systemd/system/", "644"] ] diff --git a/README.md b/README.md index bcf73cac..33b2b76e 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,6 @@ The above command will create a receiver named ```Librespot```, with bitrate set A full list of runtime options are 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`._ - ## Contact Come and hang out on gitter if you need help or want to offer some. https://gitter.im/librespot-org/spotify-connect-resources diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 636194a8..8e076ebc 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -18,70 +18,70 @@ use tokio::sync::{mpsc, oneshot}; 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. -/// 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; +// 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. -/// 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; +// 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. -/// 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); +const INITIAL_PING_TIME_ESTIMATE_SECONDS: f64 = 0.5; +// The pig time that is used for calculations before a ping time was actually measured. -/// 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); +const MAXIMUM_ASSUMED_PING_TIME_SECONDS: f64 = 1.5; +// 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. -/// 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 -/// of audio data may be larger or smaller. -pub const READ_AHEAD_BEFORE_PLAYBACK: Duration = Duration::from_secs(1); +pub const READ_AHEAD_BEFORE_PLAYBACK_SECONDS: f64 = 1.0; +// 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 +// of audio data may be larger or smaller. -/// Same as `READ_AHEAD_BEFORE_PLAYBACK`, but the time is taken as a factor of the ping -/// time to the Spotify server. Both `READ_AHEAD_BEFORE_PLAYBACK` and -/// `READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS` are obeyed. -/// Note: the calculations are done using the nominal bitrate of the file. The actual amount -/// of audio data may be larger or smaller. -pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f32 = 2.0; +pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f64 = 2.0; +// Same as READ_AHEAD_BEFORE_PLAYBACK_SECONDS, but the time is taken as a factor of the ping +// time to the Spotify server. +// Both, READ_AHEAD_BEFORE_PLAYBACK_SECONDS and READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS are +// obeyed. +// Note: the calculations are done using the nominal bitrate of the file. The actual amount +// of audio data may be larger or smaller. -/// While playing back, this many seconds of data ahead of the current read position are -/// requested. -/// Note: the calculations are done using the nominal bitrate of the file. The actual amount -/// of audio data may be larger or smaller. -pub const READ_AHEAD_DURING_PLAYBACK: Duration = Duration::from_secs(5); +pub const READ_AHEAD_DURING_PLAYBACK_SECONDS: f64 = 5.0; +// While playing back, this many seconds of data ahead of the current read position are +// requested. +// Note: the calculations are done using the nominal bitrate of the file. The actual amount +// of audio data may be larger or smaller. -/// Same as `READ_AHEAD_DURING_PLAYBACK`, but the time is taken as a factor of the ping -/// time to the Spotify server. -/// Note: the calculations are done using the nominal bitrate of the file. The actual amount -/// of audio data may be larger or smaller. -pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f32 = 10.0; +pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f64 = 10.0; +// Same as READ_AHEAD_DURING_PLAYBACK_SECONDS, but the time is taken as a factor of the ping +// time to the Spotify server. +// Note: the calculations are done using the nominal bitrate of the file. The actual amount +// of audio data may be larger or smaller. -/// 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; +const PREFETCH_THRESHOLD_FACTOR: f64 = 4.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 * * -/// Similar to `PREFETCH_THRESHOLD_FACTOR`, but it also takes the current download rate into account. -/// The formula used is ` < FAST_PREFETCH_THRESHOLD_FACTOR * * ` -/// This mechanism allows for fast downloading of the remainder of the file. The number should be larger -/// than `1.0` so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster -/// 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; +const FAST_PREFETCH_THRESHOLD_FACTOR: f64 = 1.5; +// Similar to PREFETCH_THRESHOLD_FACTOR, but it also takes the current download rate into account. +// The formula used is +// < FAST_PREFETCH_THRESHOLD_FACTOR * * +// This mechanism allows for fast downloading of the remainder of the file. The number should be larger +// than 1 so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster +// 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 cause the download rate to collapse and effectively +// only PREFETCH_THRESHOLD_FACTOR is in effect. Thus, set to zero if bandwidth saturation is not wanted. -/// 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 -/// 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; - -/// The time we will wait to obtain status updates on downloading. -const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(1); +// 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 +// 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. pub enum AudioFile { Cached(fs::File), @@ -131,10 +131,10 @@ impl StreamLoaderController { }) } - pub fn ping_time(&self) -> Duration { - Duration::from_millis(self.stream_shared.as_ref().map_or(0, |shared| { - shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64 - })) + pub fn ping_time_ms(&self) -> usize { + self.stream_shared.as_ref().map_or(0, |shared| { + shared.ping_time_ms.load(atomic::Ordering::Relaxed) + }) } fn send_stream_loader_command(&self, command: StreamLoaderCommand) { @@ -170,7 +170,7 @@ impl StreamLoaderController { { download_status = shared .cond - .wait_timeout(download_status, DOWNLOAD_TIMEOUT) + .wait_timeout(download_status, Duration::from_millis(1000)) .unwrap() .0; if range.length @@ -271,10 +271,10 @@ impl AudioFile { let mut initial_data_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_SECONDS * bytes_per_second as f64) as usize, + (INITIAL_PING_TIME_ESTIMATE_SECONDS * READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS - * bytes_per_second as f32) as usize, + * bytes_per_second as f64) as usize, ) } else { INITIAL_DOWNLOAD_SIZE @@ -368,7 +368,7 @@ impl AudioFileStreaming { let read_file = write_file.reopen().unwrap(); - // let (seek_tx, seek_rx) = mpsc::unbounded(); + //let (seek_tx, seek_rx) = mpsc::unbounded(); let (stream_loader_command_tx, stream_loader_command_rx) = mpsc::unbounded_channel::(); @@ -405,19 +405,17 @@ impl Read for AudioFileStreaming { 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. - let ping_time_seconds = Duration::from_millis( - self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64, - ) - .as_secs_f32(); + // Due to the read-ahead stuff, we potentially request more than the actual reqeust demanded. + let ping_time_seconds = + 0.0001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; let length_to_request = length + max( - (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() - * self.shared.stream_data_rate as f32) as usize, + (READ_AHEAD_DURING_PLAYBACK_SECONDS * self.shared.stream_data_rate as f64) + as usize, (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS * ping_time_seconds - * self.shared.stream_data_rate as f32) as usize, + * self.shared.stream_data_rate as f64) as usize, ); min(length_to_request, self.shared.file_size - offset) } @@ -451,7 +449,7 @@ impl Read for AudioFileStreaming { download_status = self .shared .cond - .wait_timeout(download_status, DOWNLOAD_TIMEOUT) + .wait_timeout(download_status, Duration::from_millis(1000)) .unwrap() .0; } diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 5de90b79..0f056c96 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -1,14 +1,12 @@ use std::cmp::{max, min}; use std::io::{Seek, SeekFrom, Write}; use std::sync::{atomic, Arc}; -use std::time::{Duration, Instant}; +use std::time::Instant; -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; @@ -18,7 +16,7 @@ use crate::range_set::{Range, RangeSet}; use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand}; use super::{ - FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, + FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME_SECONDS, MAX_PREFETCH_REQUESTS, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, }; @@ -48,7 +46,7 @@ pub fn request_range(session: &Session, file: FileId, offset: usize, length: usi data.write_u32::(start as u32).unwrap(); data.write_u32::(end as u32).unwrap(); - session.send_packet(PacketType::StreamChunk, data); + session.send_packet(0x8, data); channel } @@ -59,7 +57,7 @@ struct PartialFileData { } enum ReceivedData { - ResponseTime(Duration), + ResponseTimeMs(usize), Data(PartialFileData), } @@ -76,7 +74,7 @@ async fn receive_data( let old_number_of_request = shared .number_of_open_requests - .fetch_add(1, Ordering::SeqCst); + .fetch_add(1, atomic::Ordering::SeqCst); let mut measure_ping_time = old_number_of_request == 0; @@ -88,11 +86,14 @@ async fn receive_data( }; if measure_ping_time { - let mut duration = Instant::now() - request_sent_time; - if duration > MAXIMUM_ASSUMED_PING_TIME { - duration = MAXIMUM_ASSUMED_PING_TIME; + let duration = Instant::now() - request_sent_time; + let duration_ms: u64; + if 0.001 * (duration.as_millis() as f64) > MAXIMUM_ASSUMED_PING_TIME_SECONDS { + duration_ms = (MAXIMUM_ASSUMED_PING_TIME_SECONDS * 1000.0) as u64; + } else { + duration_ms = duration.as_millis() as u64; } - let _ = file_data_tx.send(ReceivedData::ResponseTime(duration)); + let _ = file_data_tx.send(ReceivedData::ResponseTimeMs(duration_ms as usize)); measure_ping_time = false; } let data_size = data.len(); @@ -126,7 +127,7 @@ async fn receive_data( shared .number_of_open_requests - .fetch_sub(1, Ordering::SeqCst); + .fetch_sub(1, atomic::Ordering::SeqCst); if result.is_err() { warn!( @@ -148,7 +149,7 @@ struct AudioFileFetch { file_data_tx: mpsc::UnboundedSender, complete_tx: Option>, - network_response_times: Vec, + network_response_times_ms: Vec, } // Might be replaced by enum from std once stable @@ -236,7 +237,7 @@ impl AudioFileFetch { // download data from after the current read position first let mut tail_end = RangeSet::new(); - let read_position = self.shared.read_position.load(Ordering::Relaxed); + let read_position = self.shared.read_position.load(atomic::Ordering::Relaxed); tail_end.add_range(&Range::new( read_position, self.shared.file_size - read_position, @@ -266,23 +267,26 @@ 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()); - - // 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); - } + ReceivedData::ResponseTimeMs(response_time_ms) => { + trace!("Ping time estimated as: {} ms.", response_time_ms); // record the response time - self.network_response_times.push(response_time); + self.network_response_times_ms.push(response_time_ms); + + // prune old response times. Keep at most three. + while self.network_response_times_ms.len() > 3 { + self.network_response_times_ms.remove(0); + } // 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, + let ping_time_ms: usize = match self.network_response_times_ms.len() { + 1 => self.network_response_times_ms[0] as usize, + 2 => { + ((self.network_response_times_ms[0] + self.network_response_times_ms[1]) + / 2) as usize + } 3 => { - let mut times = self.network_response_times.clone(); + let mut times = self.network_response_times_ms.clone(); times.sort_unstable(); times[1] } @@ -292,7 +296,7 @@ impl AudioFileFetch { // 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, atomic::Ordering::Relaxed); } ReceivedData::Data(data) => { self.output @@ -386,7 +390,7 @@ pub(super) async fn audio_file_fetch( file_data_tx, complete_tx: Some(complete_tx), - network_response_times: Vec::with_capacity(3), + network_response_times_ms: Vec::new(), }; loop { @@ -404,8 +408,10 @@ pub(super) async fn audio_file_fetch( } if fetch.get_download_strategy() == DownloadStrategy::Streaming() { - let number_of_open_requests = - fetch.shared.number_of_open_requests.load(Ordering::SeqCst); + let number_of_open_requests = fetch + .shared + .number_of_open_requests + .load(atomic::Ordering::SeqCst); if number_of_open_requests < MAX_PREFETCH_REQUESTS { let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests; @@ -418,15 +424,14 @@ pub(super) async fn audio_file_fetch( }; let ping_time_seconds = - Duration::from_millis(fetch.shared.ping_time_ms.load(Ordering::Relaxed) as u64) - .as_secs_f32(); + 0.001 * fetch.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; let download_rate = fetch.session.channel().get_download_rate_estimate(); let desired_pending_bytes = max( (PREFETCH_THRESHOLD_FACTOR * ping_time_seconds - * fetch.shared.stream_data_rate as f32) as usize, - (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f32) + * fetch.shared.stream_data_rate as f64) as usize, + (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f64) as usize, ); diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 4b486bbe..e43cf728 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -11,6 +11,6 @@ mod range_set; pub use decrypt::AudioDecrypt; pub use fetch::{AudioFile, StreamLoaderController}; pub use fetch::{ - READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, - READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, + READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, + READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 89d185ab..8e9589fc 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -8,15 +8,25 @@ repository = "https://github.com/librespot-org/librespot" edition = "2018" [dependencies] +aes-ctr = "0.6" +base64 = "0.13" form_urlencoded = "1.0" +futures-core = "0.3" futures-util = { version = "0.3.5", default_features = false } +hmac = "0.11" +hyper = { version = "0.14", features = ["server", "http1", "tcp"] } +libmdns = "0.6" log = "0.4" protobuf = "2.14.0" rand = "0.8" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -tokio = { version = "1.0", features = ["macros", "sync"] } +serde_json = "1.0.25" +sha-1 = "0.9" +tokio = { version = "1.0", features = ["macros", "rt", "sync"] } tokio-stream = "0.1.1" +url = "2.1" + +dns-sd = { version = "0.1.3", optional = true } [dependencies.librespot-core] path = "../core" @@ -30,9 +40,6 @@ version = "0.2.0" path = "../protocol" version = "0.2.0" -[dependencies.librespot-discovery] -path = "../discovery" -version = "0.2.0" - [features] -with-dns-sd = ["librespot-discovery/with-dns-sd"] +with-dns-sd = ["dns-sd"] + diff --git a/connect/src/discovery.rs b/connect/src/discovery.rs index 8ce3f4f0..7d559f0a 100644 --- a/connect/src/discovery.rs +++ b/connect/src/discovery.rs @@ -1,19 +1,203 @@ -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; +use aes_ctr::cipher::generic_array::GenericArray; +use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; +use aes_ctr::Aes128Ctr; +use futures_core::Stream; +use hmac::{Hmac, Mac, NewMac}; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Method, Request, Response, StatusCode}; +use serde_json::json; +use sha1::{Digest, Sha1}; +use tokio::sync::{mpsc, oneshot}; + +#[cfg(feature = "with-dns-sd")] +use dns_sd::DNSService; -use futures_util::Stream; use librespot_core::authentication::Credentials; use librespot_core::config::ConnectConfig; +use librespot_core::diffie_hellman::DhLocalKeys; -pub struct DiscoveryStream(librespot_discovery::Discovery); +use std::borrow::Cow; +use std::collections::BTreeMap; +use std::convert::Infallible; +use std::io; +use std::net::{Ipv4Addr, SocketAddr}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; -impl Stream for DiscoveryStream { - type Item = Credentials; +type HmacSha1 = Hmac; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.0).poll_next(cx) +#[derive(Clone)] +struct Discovery(Arc); +struct DiscoveryInner { + config: ConnectConfig, + device_id: String, + keys: DhLocalKeys, + tx: mpsc::UnboundedSender, +} + +impl Discovery { + fn new( + config: ConnectConfig, + device_id: String, + ) -> (Discovery, mpsc::UnboundedReceiver) { + let (tx, rx) = mpsc::unbounded_channel(); + + let discovery = Discovery(Arc::new(DiscoveryInner { + config, + device_id, + keys: DhLocalKeys::random(&mut rand::thread_rng()), + tx, + })); + + (discovery, rx) } + + fn handle_get_info(&self, _: BTreeMap, Cow<'_, str>>) -> Response { + let public_key = base64::encode(&self.0.keys.public_key()); + + let result = json!({ + "status": 101, + "statusString": "ERROR-OK", + "spotifyError": 0, + "version": "2.7.1", + "deviceID": (self.0.device_id), + "remoteName": (self.0.config.name), + "activeUser": "", + "publicKey": (public_key), + "deviceType": (self.0.config.device_type.to_string().to_uppercase()), + "libraryVersion": "0.1.0", + "accountReq": "PREMIUM", + "brandDisplayName": "librespot", + "modelDisplayName": "librespot", + "resolverVersion": "0", + "groupStatus": "NONE", + "voiceSupport": "NO", + }); + + let body = result.to_string(); + Response::new(Body::from(body)) + } + + fn handle_add_user( + &self, + params: BTreeMap, Cow<'_, str>>, + ) -> Response { + let username = params.get("userName").unwrap().as_ref(); + let encrypted_blob = params.get("blob").unwrap(); + let client_key = params.get("clientKey").unwrap(); + + let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); + + let shared_key = self + .0 + .keys + .shared_secret(&base64::decode(client_key.as_bytes()).unwrap()); + + let iv = &encrypted_blob[0..16]; + let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; + let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; + + let base_key = Sha1::digest(&shared_key); + let base_key = &base_key[..16]; + + let checksum_key = { + let mut h = HmacSha1::new_from_slice(base_key).expect("HMAC can take key of any size"); + h.update(b"checksum"); + h.finalize().into_bytes() + }; + + let encryption_key = { + let mut h = HmacSha1::new_from_slice(&base_key).expect("HMAC can take key of any size"); + h.update(b"encryption"); + h.finalize().into_bytes() + }; + + let mut h = HmacSha1::new_from_slice(&checksum_key).expect("HMAC can take key of any size"); + h.update(encrypted); + if h.verify(cksum).is_err() { + warn!("Login error for user {:?}: MAC mismatch", username); + let result = json!({ + "status": 102, + "spotifyError": 1, + "statusString": "ERROR-MAC" + }); + + let body = result.to_string(); + return Response::new(Body::from(body)); + } + + let decrypted = { + let mut data = encrypted.to_vec(); + let mut cipher = Aes128Ctr::new( + &GenericArray::from_slice(&encryption_key[0..16]), + &GenericArray::from_slice(iv), + ); + cipher.apply_keystream(&mut data); + String::from_utf8(data).unwrap() + }; + + let credentials = + Credentials::with_blob(username.to_string(), &decrypted, &self.0.device_id); + + self.0.tx.send(credentials).unwrap(); + + let result = json!({ + "status": 101, + "spotifyError": 0, + "statusString": "ERROR-OK" + }); + + let body = result.to_string(); + Response::new(Body::from(body)) + } + + fn not_found(&self) -> Response { + let mut res = Response::default(); + *res.status_mut() = StatusCode::NOT_FOUND; + res + } + + async fn call(self, request: Request) -> hyper::Result> { + let mut params = BTreeMap::new(); + + let (parts, body) = request.into_parts(); + + if let Some(query) = parts.uri.query() { + let query_params = url::form_urlencoded::parse(query.as_bytes()); + params.extend(query_params); + } + + if parts.method != Method::GET { + debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params); + } + + let body = hyper::body::to_bytes(body).await?; + + params.extend(url::form_urlencoded::parse(&body)); + + Ok( + match (parts.method, params.get("action").map(AsRef::as_ref)) { + (Method::GET, Some("getInfo")) => self.handle_get_info(params), + (Method::POST, Some("addUser")) => self.handle_add_user(params), + _ => self.not_found(), + }, + ) + } +} + +#[cfg(feature = "with-dns-sd")] +pub struct DiscoveryStream { + credentials: mpsc::UnboundedReceiver, + _svc: DNSService, + _close_tx: oneshot::Sender, +} + +#[cfg(not(feature = "with-dns-sd"))] +pub struct DiscoveryStream { + credentials: mpsc::UnboundedReceiver, + _svc: libmdns::Service, + _close_tx: oneshot::Sender, } pub fn discovery( @@ -21,11 +205,59 @@ pub fn discovery( device_id: String, port: u16, ) -> io::Result { - librespot_discovery::Discovery::builder(device_id) - .device_type(config.device_type) - .port(port) - .name(config.name) - .launch() - .map(DiscoveryStream) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + let (discovery, creds_rx) = Discovery::new(config.clone(), device_id); + let (close_tx, close_rx) = oneshot::channel(); + + let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port); + + let make_service = make_service_fn(move |_| { + let discovery = discovery.clone(); + async move { Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().call(request))) } + }); + + let server = hyper::Server::bind(&address).serve(make_service); + + let s_port = server.local_addr().port(); + debug!("Zeroconf server listening on 0.0.0.0:{}", s_port); + + tokio::spawn(server.with_graceful_shutdown(async { + close_rx.await.unwrap_err(); + debug!("Shutting down discovery server"); + })); + + #[cfg(feature = "with-dns-sd")] + let svc = DNSService::register( + Some(&*config.name), + "_spotify-connect._tcp", + None, + None, + s_port, + &["VERSION=1.0", "CPath=/"], + ) + .unwrap(); + + #[cfg(not(feature = "with-dns-sd"))] + let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; + + #[cfg(not(feature = "with-dns-sd"))] + let svc = responder.register( + "_spotify-connect._tcp".to_owned(), + config.name, + s_port, + &["VERSION=1.0", "CPath=/"], + ); + + Ok(DiscoveryStream { + credentials: creds_rx, + _svc: svc, + _close_tx: close_tx, + }) +} + +impl Stream for DiscoveryStream { + type Item = Credentials; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.credentials.poll_recv(cx) + } } diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 267bf1b8..600dd033 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -6,9 +6,5 @@ use librespot_playback as playback; use librespot_protocol as protocol; pub mod context; -#[deprecated( - since = "0.2.1", - note = "Please use the crate `librespot_discovery` instead." -)] pub mod discovery; pub mod spirc; diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 57dc4cdd..eeb840d2 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -3,7 +3,7 @@ use std::pin::Pin; use std::time::{SystemTime, UNIX_EPOCH}; use crate::context::StationContext; -use crate::core::config::ConnectConfig; +use crate::core::config::{ConnectConfig, VolumeCtrl}; use crate::core::mercury::{MercuryError, MercurySender}; use crate::core::session::Session; use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError}; @@ -54,6 +54,7 @@ struct SpircTask { device: DeviceState, state: State, play_request_id: Option, + mixer_started: bool, play_status: SpircPlayStatus, subscription: BoxedStream, @@ -81,15 +82,13 @@ pub enum SpircCommand { } struct SpircTaskConfig { + volume_ctrl: VolumeCtrl, autoplay: bool, } const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; -const VOLUME_STEPS: i64 = 64; -const VOLUME_STEP_SIZE: u16 = 1024; // (u16::MAX + 1) / VOLUME_STEPS - pub struct Spirc { commands: mpsc::UnboundedSender, } @@ -164,10 +163,10 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState { msg.set_typ(protocol::spirc::CapabilityType::kVolumeSteps); { let repeated = msg.mut_intValue(); - if config.has_volume_ctrl { - repeated.push(VOLUME_STEPS) - } else { + if let VolumeCtrl::Fixed = config.volume_ctrl { repeated.push(0) + } else { + repeated.push(64) } }; msg @@ -215,6 +214,36 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState { } } +fn calc_logarithmic_volume(volume: u16) -> u16 { + // Volume conversion taken from https://www.dr-lex.be/info-stuff/volumecontrols.html#ideal2 + // Convert the given volume [0..0xffff] to a dB gain + // We assume a dB range of 60dB. + // Use the equation: a * exp(b * x) + // in which a = IDEAL_FACTOR, b = 1/1000 + const IDEAL_FACTOR: f64 = 6.908; + let normalized_volume = volume as f64 / std::u16::MAX as f64; // To get a value between 0 and 1 + + let mut val = std::u16::MAX; + // Prevent val > std::u16::MAX due to rounding errors + if normalized_volume < 0.999 { + let new_volume = (normalized_volume * IDEAL_FACTOR).exp() / 1000.0; + val = (new_volume * std::u16::MAX as f64) as u16; + } + + debug!("input volume:{} to mixer: {}", volume, val); + + // return the scale factor (0..0xffff) (equivalent to a voltage multiplier). + val +} + +fn volume_to_mixer(volume: u16, volume_ctrl: &VolumeCtrl) -> u16 { + match volume_ctrl { + VolumeCtrl::Linear => volume, + VolumeCtrl::Log => calc_logarithmic_volume(volume), + VolumeCtrl::Fixed => volume, + } +} + fn url_encode(bytes: impl AsRef<[u8]>) -> String { form_urlencoded::byte_serialize(bytes.as_ref()).collect() } @@ -251,8 +280,9 @@ impl Spirc { let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); - let initial_volume = config.initial_volume; + let volume = config.volume; let task_config = SpircTaskConfig { + volume_ctrl: config.volume_ctrl.to_owned(), autoplay: config.autoplay, }; @@ -272,6 +302,7 @@ impl Spirc { device, state: initial_state(), play_request_id: None, + mixer_started: false, play_status: SpircPlayStatus::Stopped, subscription, @@ -287,12 +318,7 @@ impl Spirc { context: None, }; - if let Some(volume) = initial_volume { - task.set_volume(volume); - } else { - let current_volume = task.mixer.volume(); - task.set_volume(current_volume); - } + task.set_volume(volume); let spirc = Spirc { commands: cmd_tx }; @@ -411,6 +437,20 @@ impl SpircTask { dur.as_millis() as i64 + 1000 * self.session.time_delta() } + fn ensure_mixer_started(&mut self) { + if !self.mixer_started { + self.mixer.start(); + self.mixer_started = true; + } + } + + fn ensure_mixer_stopped(&mut self) { + if self.mixer_started { + self.mixer.stop(); + self.mixer_started = false; + } + } + fn update_state_position(&mut self, position_ms: u32) { let now = self.now_ms(); self.state.set_position_measured_at(now as u64); @@ -560,6 +600,7 @@ impl SpircTask { _ => { warn!("The player has stopped unexpectedly."); self.state.set_status(PlayStatus::kPlayStatusStop); + self.ensure_mixer_stopped(); self.notify(None, true); self.play_status = SpircPlayStatus::Stopped; } @@ -618,6 +659,7 @@ impl SpircTask { info!("No more tracks left in queue"); self.state.set_status(PlayStatus::kPlayStatusStop); self.player.stop(); + self.mixer.stop(); self.play_status = SpircPlayStatus::Stopped; } @@ -725,6 +767,7 @@ impl SpircTask { self.device.set_is_active(false); self.state.set_status(PlayStatus::kPlayStatusStop); self.player.stop(); + self.ensure_mixer_stopped(); self.play_status = SpircPlayStatus::Stopped; } } @@ -739,11 +782,7 @@ impl SpircTask { position_ms, preloading_of_next_track_triggered, } => { - // 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.ensure_mixer_started(); self.player.play(); self.state.set_status(PlayStatus::kPlayStatusPlay); self.update_state_position(position_ms); @@ -753,6 +792,7 @@ impl SpircTask { }; } SpircPlayStatus::LoadingPause { position_ms } => { + self.ensure_mixer_started(); self.player.play(); self.play_status = SpircPlayStatus::LoadingPlay { position_ms }; } @@ -922,6 +962,7 @@ impl SpircTask { self.state.set_playing_track_index(0); self.state.set_status(PlayStatus::kPlayStatusStop); self.player.stop(); + self.ensure_mixer_stopped(); self.play_status = SpircPlayStatus::Stopped; } } @@ -966,13 +1007,19 @@ impl SpircTask { } fn handle_volume_up(&mut self) { - let volume = (self.device.get_volume() as u16).saturating_add(VOLUME_STEP_SIZE); - self.set_volume(volume); + let mut volume: u32 = self.device.get_volume() as u32 + 4096; + if volume > 0xFFFF { + volume = 0xFFFF; + } + self.set_volume(volume as u16); } fn handle_volume_down(&mut self) { - let volume = (self.device.get_volume() as u16).saturating_sub(VOLUME_STEP_SIZE); - self.set_volume(volume); + let mut volume: i32 = self.device.get_volume() as i32 - 4096; + if volume < 0 { + volume = 0; + } + self.set_volume(volume as u16); } fn handle_end_of_track(&mut self) { @@ -1196,6 +1243,7 @@ impl SpircTask { None => { self.state.set_status(PlayStatus::kPlayStatusStop); self.player.stop(); + self.ensure_mixer_stopped(); self.play_status = SpircPlayStatus::Stopped; } } @@ -1225,7 +1273,8 @@ impl SpircTask { fn set_volume(&mut self, volume: u16) { self.device.set_volume(volume as u32); - self.mixer.set_volume(volume); + self.mixer + .set_volume(volume_to_mixer(volume, &self.config.volume_ctrl)); if let Some(cache) = self.session.cache() { cache.save_volume(volume) } diff --git a/contrib/librespot.service b/contrib/librespot.service index 76037c8c..bd381df2 100644 --- a/contrib/librespot.service +++ b/contrib/librespot.service @@ -1,7 +1,5 @@ [Unit] -Description=Librespot (an open source Spotify client) -Documentation=https://github.com/librespot-org/librespot -Documentation=https://github.com/librespot-org/librespot/wiki/Options +Description=Librespot Requires=network-online.target After=network-online.target @@ -10,7 +8,8 @@ User=nobody Group=audio Restart=always RestartSec=10 -ExecStart=/usr/bin/librespot --name "%p@%H" +ExecStart=/usr/bin/librespot -n "%p on %H" [Install] WantedBy=multi-user.target + diff --git a/contrib/librespot.user.service b/contrib/librespot.user.service deleted file mode 100644 index a676dde0..00000000 --- a/contrib/librespot.user.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Librespot (an open source Spotify client) -Documentation=https://github.com/librespot-org/librespot -Documentation=https://github.com/librespot-org/librespot/wiki/Options - -[Service] -Restart=always -RestartSec=10 -ExecStart=/usr/bin/librespot --name "%u@%H" - -[Install] -WantedBy=default.target diff --git a/core/Cargo.toml b/core/Cargo.toml index 3c239034..7eb4051c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -26,9 +26,7 @@ http = "0.2" hyper = { version = "0.14", features = ["client", "tcp", "http1"] } hyper-proxy = { version = "0.9.1", default-features = false } log = "0.4" -num = "0.4" num-bigint = { version = "0.4", features = ["rand"] } -num-derive = "0.3" num-integer = "0.1" num-traits = "0.2" once_cell = "1.5.2" diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 623c7cb3..975e0e18 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,141 +1,132 @@ -use hyper::{Body, Request}; -use serde::Deserialize; use std::error::Error; -use std::sync::atomic::{AtomicUsize, Ordering}; + +use hyper::client::HttpConnector; +use hyper::{Body, Client, Request}; +use hyper_proxy::{Intercept, Proxy, ProxyConnector}; +use serde::Deserialize; +use url::Url; + +const APRESOLVE_ENDPOINT: &str = + "http://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient"; + +// These addresses probably do some geo-location based traffic management or at least DNS-based +// load balancing. They are known to fail when the normal resolvers are up, so that's why they +// should only be used as fallback. +const AP_FALLBACK: &str = "ap.spotify.com"; +const DEALER_FALLBACK: &str = "dealer.spotify.com"; +const SPCLIENT_FALLBACK: &str = "spclient.wg.spotify.com"; + +const FALLBACK_PORT: u16 = 443; pub type SocketAddress = (String, u16); -#[derive(Default)] -struct AccessPoints { - accesspoint: Vec, - dealer: Vec, - spclient: Vec, -} - -#[derive(Deserialize)] +#[derive(Clone, Debug, Default, Deserialize)] struct ApResolveData { accesspoint: Vec, dealer: Vec, spclient: Vec, } -// These addresses probably do some geo-location based traffic management or at least DNS-based -// load balancing. They are known to fail when the normal resolvers are up, so that's why they -// should only be used as fallback. -impl Default for ApResolveData { - fn default() -> Self { - Self { - accesspoint: vec![String::from("ap.spotify.com:443")], - dealer: vec![String::from("dealer.spotify.com:443")], - spclient: vec![String::from("spclient.wg.spotify.com:443")], - } +#[derive(Clone, Debug, Deserialize)] +pub struct AccessPoints { + pub accesspoint: SocketAddress, + pub dealer: SocketAddress, + pub spclient: SocketAddress, +} + +fn select_ap(data: Vec, fallback: &str, ap_port: Option) -> SocketAddress { + let port = ap_port.unwrap_or(FALLBACK_PORT); + + let mut aps = data.into_iter().filter_map(|ap| { + let mut split = ap.rsplitn(2, ':'); + let port = split + .next() + .expect("rsplitn should not return empty iterator"); + let host = split.next()?.to_owned(); + let port: u16 = port.parse().ok()?; + Some((host, port)) + }); + + let ap = if ap_port.is_some() { + aps.find(|(_, p)| *p == port) + } else { + aps.next() + }; + + ap.unwrap_or_else(|| (String::from(fallback), port)) +} + +async fn try_apresolve(proxy: Option<&Url>) -> Result> { + let req = Request::builder() + .method("GET") + .uri(APRESOLVE_ENDPOINT) + .body(Body::empty()) + .unwrap(); + + let response = if let Some(url) = proxy { + // Panic safety: all URLs are valid URIs + let uri = url.to_string().parse().unwrap(); + 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? + } else { + Client::new().request(req).await? + }; + + let body = hyper::body::to_bytes(response.into_body()).await?; + let data: ApResolveData = serde_json::from_slice(body.as_ref())?; + + Ok(data) +} + +pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> AccessPoints { + let data = try_apresolve(proxy).await.unwrap_or_else(|e| { + warn!("Failed to resolve access points: {}, using fallbacks.", e); + ApResolveData::default() + }); + + let accesspoint = select_ap(data.accesspoint, AP_FALLBACK, ap_port); + let dealer = select_ap(data.dealer, DEALER_FALLBACK, ap_port); + let spclient = select_ap(data.spclient, SPCLIENT_FALLBACK, ap_port); + + AccessPoints { + accesspoint, + dealer, + spclient, } } -component! { - ApResolver : ApResolverInner { - data: AccessPoints = AccessPoints::default(), - spinlock: AtomicUsize = AtomicUsize::new(0), - } -} - -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 { - if self.session().config().proxy.is_some() || self.session().config().ap_port.is_some() { - Some(self.session().config().ap_port.unwrap_or(443)) - } else { - None - } - } - - fn process_data(&self, data: Vec) -> Vec { - data.into_iter() - .filter_map(|ap| { - let mut split = ap.rsplitn(2, ':'); - let port = split - .next() - .expect("rsplitn should not return empty iterator"); - let host = split.next()?.to_owned(); - let port: u16 = port.parse().ok()?; - if let Some(p) = self.port_config() { - if p != port { - return None; - } - } - Some((host, port)) - }) - .collect() - } - - 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(); - - let body = self.session().http_client().request_body(req).await?; - let data: ApResolveData = serde_json::from_slice(body.as_ref())?; - - Ok(data) - } - - async fn apresolve(&self) { - let result = self.try_apresolve().await; - - self.lock(|inner| { - let data = match result { - Ok(data) => data, - Err(e) => { - warn!("Failed to resolve access points, using fallbacks: {}", e); - ApResolveData::default() - } - }; - - inner.data.accesspoint = self.process_data(data.accesspoint); - inner.data.dealer = self.process_data(data.dealer); - inner.data.spclient = self.process_data(data.spclient); - }) - } - - fn is_empty(&self) -> bool { - self.lock(|inner| { - inner.data.accesspoint.is_empty() - || inner.data.dealer.is_empty() - || inner.data.spclient.is_empty() - }) - } - - pub async fn resolve(&self, endpoint: &str) -> SocketAddress { - // Use a spinlock to make this function atomic. Otherwise, various race conditions may - // occur, e.g. when the session is created, multiple components are launched almost in - // parallel and they will all call this function, while resolving is still in progress. - self.lock(|inner| { - while inner.spinlock.load(Ordering::SeqCst) != 0 { - #[allow(deprecated)] - std::sync::atomic::spin_loop_hint() - } - inner.spinlock.store(1, Ordering::SeqCst); - }); - - if self.is_empty() { - self.apresolve().await; - } - - self.lock(|inner| { - let access_point = match endpoint { - // take the first position instead of the last with `pop`, because Spotify returns - // access points with ports 4070, 443 and 80 in order of preference from highest - // to lowest. - "accesspoint" => inner.data.accesspoint.remove(0), - "dealer" => inner.data.dealer.remove(0), - "spclient" => inner.data.spclient.remove(0), - _ => unimplemented!(), - }; - inner.spinlock.store(0, Ordering::SeqCst); - access_point - }) +#[cfg(test)] +mod test { + use std::net::ToSocketAddrs; + + use super::apresolve; + + #[tokio::test] + async fn test_apresolve() { + let aps = apresolve(None, None).await; + + // Assert that the result contains a valid host and port + aps.accesspoint.to_socket_addrs().unwrap().next().unwrap(); + aps.dealer.to_socket_addrs().unwrap().next().unwrap(); + aps.spclient.to_socket_addrs().unwrap().next().unwrap(); + } + + #[tokio::test] + async fn test_apresolve_port_443() { + let aps = apresolve(None, Some(443)).await; + + let port = aps + .accesspoint + .to_socket_addrs() + .unwrap() + .next() + .unwrap() + .port(); + assert_eq!(port, 443); } } diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index f42c6502..3bce1c73 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -4,7 +4,6 @@ use std::collections::HashMap; use std::io::Write; use tokio::sync::oneshot; -use crate::packet::PacketType; use crate::spotify_id::{FileId, SpotifyId}; use crate::util::SeqGenerator; @@ -22,19 +21,19 @@ component! { } impl AudioKeyManager { - pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { let seq = BigEndian::read_u32(data.split_to(4).as_ref()); let sender = self.lock(|inner| inner.pending.remove(&seq)); if let Some(sender) = sender { match cmd { - PacketType::AesKey => { + 0xd => { let mut key = [0u8; 16]; key.copy_from_slice(data.as_ref()); let _ = sender.send(Ok(AudioKey(key))); } - PacketType::AesKeyError => { + 0xe => { warn!( "error audio key {:x} {:x}", data.as_ref()[0], @@ -62,11 +61,11 @@ impl AudioKeyManager { fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) { let mut data: Vec = Vec::new(); - data.write_all(&file.0).unwrap(); - data.write_all(&track.to_raw()).unwrap(); + data.write(&file.0).unwrap(); + data.write(&track.to_raw()).unwrap(); data.write_u32::(seq).unwrap(); data.write_u16::(0x0000).unwrap(); - self.session().send_packet(PacketType::RequestKey, data) + self.session().send_packet(0xc, data) } } diff --git a/core/src/channel.rs b/core/src/channel.rs index 31c01a40..4a78a4aa 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -8,10 +8,8 @@ use bytes::Bytes; use futures_core::Stream; use futures_util::lock::BiLock; use futures_util::{ready, StreamExt}; -use num_traits::FromPrimitive; use tokio::sync::mpsc; -use crate::packet::PacketType; use crate::util::SeqGenerator; component! { @@ -25,8 +23,6 @@ component! { } } -const ONE_SECOND_IN_MS: usize = 1000; - #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] pub struct ChannelError; @@ -70,7 +66,7 @@ impl ChannelManager { (seq, channel) } - pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: u8, mut data: Bytes) { use std::collections::hash_map::Entry; let id: u16 = BigEndian::read_u16(data.split_to(2).as_ref()); @@ -78,11 +74,8 @@ impl ChannelManager { self.lock(|inner| { let current_time = Instant::now(); if let Some(download_measurement_start) = inner.download_measurement_start { - if (current_time - download_measurement_start).as_millis() - > ONE_SECOND_IN_MS as u128 - { - inner.download_rate_estimate = ONE_SECOND_IN_MS - * inner.download_measurement_bytes + if (current_time - download_measurement_start).as_millis() > 1000 { + inner.download_rate_estimate = 1000 * inner.download_measurement_bytes / (current_time - download_measurement_start).as_millis() as usize; inner.download_measurement_start = Some(current_time); inner.download_measurement_bytes = 0; @@ -94,7 +87,7 @@ 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)); + let _ = entry.get().send((cmd, data)); } }); } @@ -116,8 +109,7 @@ impl Channel { fn recv_packet(&mut self, cx: &mut Context<'_>) -> Poll> { let (cmd, packet) = ready!(self.receiver.poll_recv(cx)).ok_or(ChannelError)?; - let packet_type = FromPrimitive::from_u8(cmd); - if let Some(PacketType::ChannelError) = packet_type { + if cmd == 0xa { let code = BigEndian::read_u16(&packet.as_ref()[..2]); error!("channel error: {} {}", packet.len(), code); diff --git a/core/src/config.rs b/core/src/config.rs index 0e3eaf4a..9c70c25b 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -71,43 +71,30 @@ impl FromStr for DeviceType { } } -impl From<&DeviceType> for &str { - fn from(d: &DeviceType) -> &'static str { - use self::DeviceType::*; - match d { - Unknown => "Unknown", - Computer => "Computer", - Tablet => "Tablet", - Smartphone => "Smartphone", - Speaker => "Speaker", - Tv => "TV", - Avr => "AVR", - Stb => "STB", - AudioDongle => "AudioDongle", - GameConsole => "GameConsole", - CastAudio => "CastAudio", - CastVideo => "CastVideo", - Automobile => "Automobile", - Smartwatch => "Smartwatch", - Chromebook => "Chromebook", - UnknownSpotify => "UnknownSpotify", - CarThing => "CarThing", - Observer => "Observer", - HomeThing => "HomeThing", - } - } -} - -impl From for &str { - fn from(d: DeviceType) -> &'static str { - (&d).into() - } -} - impl fmt::Display for DeviceType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let str: &str = self.into(); - f.write_str(str) + use self::DeviceType::*; + match *self { + Unknown => f.write_str("Unknown"), + Computer => f.write_str("Computer"), + Tablet => f.write_str("Tablet"), + Smartphone => f.write_str("Smartphone"), + Speaker => f.write_str("Speaker"), + Tv => f.write_str("TV"), + Avr => f.write_str("AVR"), + Stb => f.write_str("STB"), + AudioDongle => f.write_str("AudioDongle"), + GameConsole => f.write_str("GameConsole"), + CastAudio => f.write_str("CastAudio"), + CastVideo => f.write_str("CastVideo"), + Automobile => f.write_str("Automobile"), + Smartwatch => f.write_str("Smartwatch"), + Chromebook => f.write_str("Chromebook"), + UnknownSpotify => f.write_str("UnknownSpotify"), + CarThing => f.write_str("CarThing"), + Observer => f.write_str("Observer"), + HomeThing => f.write_str("HomeThing"), + } } } @@ -121,7 +108,33 @@ impl Default for DeviceType { pub struct ConnectConfig { pub name: String, pub device_type: DeviceType, - pub initial_volume: Option, - pub has_volume_ctrl: bool, + pub volume: u16, + pub volume_ctrl: VolumeCtrl, pub autoplay: bool, } + +#[derive(Clone, Debug)] +pub enum VolumeCtrl { + Linear, + Log, + Fixed, +} + +impl FromStr for VolumeCtrl { + type Err = (); + fn from_str(s: &str) -> Result { + use self::VolumeCtrl::*; + match s.to_lowercase().as_ref() { + "linear" => Ok(Linear), + "log" => Ok(Log), + "fixed" => Ok(Fixed), + _ => Err(()), + } + } +} + +impl Default for VolumeCtrl { + fn default() -> VolumeCtrl { + VolumeCtrl::Log + } +} diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 472109e6..bacdc653 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -7,7 +7,6 @@ pub use self::handshake::handshake; use std::io::{self, ErrorKind}; use futures_util::{SinkExt, StreamExt}; -use num_traits::FromPrimitive; use protobuf::{self, Message, ProtobufError}; use thiserror::Error; use tokio::net::TcpStream; @@ -15,7 +14,6 @@ use tokio_util::codec::Framed; use url::Url; use crate::authentication::Credentials; -use crate::packet::PacketType; use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; use crate::version; @@ -97,14 +95,13 @@ pub async fn authenticate( .set_device_id(device_id.to_string()); packet.set_version_string(version::VERSION_STRING.to_string()); - let cmd = PacketType::Login; + let cmd = 0xab; let data = packet.write_to_bytes().unwrap(); - transport.send((cmd as u8, data)).await?; + transport.send((cmd, data)).await?; let (cmd, data) = transport.next().await.expect("EOF")?; - let packet_type = FromPrimitive::from_u8(cmd); - match packet_type { - Some(PacketType::APWelcome) => { + match cmd { + 0xac => { let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?; let reusable_credentials = Credentials { @@ -115,7 +112,7 @@ pub async fn authenticate( Ok(reusable_credentials) } - Some(PacketType::AuthFailure) => { + 0xad => { let error_data = APLoginFailed::parse_from_bytes(data.as_ref())?; Err(error_data.into()) } diff --git a/core/src/http_client.rs b/core/src/http_client.rs deleted file mode 100644 index 5f8ef780..00000000 --- a/core/src/http_client.rs +++ /dev/null @@ -1,34 +0,0 @@ -use hyper::client::HttpConnector; -use hyper::{Body, Client, Request, Response}; -use hyper_proxy::{Intercept, Proxy, ProxyConnector}; -use url::Url; - -pub struct HttpClient { - proxy: Option, -} - -impl HttpClient { - pub fn new(proxy: Option<&Url>) -> Self { - Self { - proxy: proxy.cloned(), - } - } - - 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(); - 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 - } else { - Client::new().request(req).await - } - } - - 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/keymaster.rs b/core/src/keymaster.rs new file mode 100644 index 00000000..8c3c00a2 --- /dev/null +++ b/core/src/keymaster.rs @@ -0,0 +1,26 @@ +use serde::Deserialize; + +use crate::{mercury::MercuryError, session::Session}; + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Token { + pub access_token: String, + pub expires_in: u32, + pub token_type: String, + pub scope: Vec, +} + +pub async fn get_token( + session: &Session, + client_id: &str, + scopes: &str, +) -> Result { + let url = format!( + "hm://keymaster/token/authenticated?client_id={}&scope={}", + client_id, scopes + ); + let response = session.mercury().get(url).await?; + let data = response.payload.first().expect("Empty payload"); + serde_json::from_slice(data.as_ref()).map_err(|_| MercuryError) +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 9c92c235..f26caf3d 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,6 +1,7 @@ +#![allow(clippy::unused_io_amount)] + #[macro_use] extern crate log; -extern crate num_derive; use librespot_protocol as protocol; @@ -18,15 +19,12 @@ mod connection; mod dealer; #[doc(hidden)] pub mod diffie_hellman; -mod http_client; +pub mod keymaster; pub mod mercury; -pub mod packet; mod proxytunnel; pub mod session; mod socket; -mod spclient; pub mod spotify_id; -mod token; #[doc(hidden)] pub mod util; pub mod version; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 6cf3519e..57650087 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -11,7 +11,6 @@ use futures_util::FutureExt; use protobuf::Message; use tokio::sync::{mpsc, oneshot}; -use crate::packet::PacketType; use crate::protocol; use crate::util::SeqGenerator; @@ -144,7 +143,7 @@ impl MercuryManager { } } - pub(crate) fn dispatch(&self, cmd: PacketType, mut data: Bytes) { + pub(crate) fn dispatch(&self, cmd: u8, 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(); @@ -155,17 +154,14 @@ impl MercuryManager { let mut pending = match pending { Some(pending) => pending, + None if cmd == 0xb5 => MercuryPending { + parts: Vec::new(), + partial: None, + callback: None, + }, None => { - if let PacketType::MercuryEvent = cmd { - MercuryPending { - parts: Vec::new(), - partial: None, - callback: None, - } - } else { - warn!("Ignore seq {:?} cmd {:x}", seq, cmd as u8); - return; - } + warn!("Ignore seq {:?} cmd {:x}", seq, cmd); + return; } }; @@ -195,7 +191,7 @@ impl MercuryManager { data.split_to(size).as_ref().to_owned() } - fn complete_request(&self, cmd: PacketType, mut pending: MercuryPending) { + fn complete_request(&self, cmd: u8, mut pending: MercuryPending) { let header_data = pending.parts.remove(0); let header = protocol::mercury::Header::parse_from_bytes(&header_data).unwrap(); @@ -212,7 +208,7 @@ impl MercuryManager { if let Some(cb) = pending.callback { let _ = cb.send(Err(MercuryError)); } - } else if let PacketType::MercuryEvent = cmd { + } else if cmd == 0xb5 { self.lock(|inner| { let mut found = false; diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 1d6b5b15..402a954c 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -2,7 +2,6 @@ use byteorder::{BigEndian, WriteBytesExt}; use protobuf::Message; use std::io::Write; -use crate::packet::PacketType; use crate::protocol; #[derive(Debug, PartialEq, Eq)] @@ -44,12 +43,11 @@ impl ToString for MercuryMethod { } impl MercuryMethod { - pub fn command(&self) -> PacketType { - use PacketType::*; + pub fn command(&self) -> u8 { match *self { - MercuryMethod::Get | MercuryMethod::Send => MercuryReq, - MercuryMethod::Sub => MercurySub, - MercuryMethod::Unsub => MercuryUnsub, + MercuryMethod::Get | MercuryMethod::Send => 0xb2, + MercuryMethod::Sub => 0xb3, + MercuryMethod::Unsub => 0xb4, } } } @@ -79,7 +77,7 @@ impl MercuryRequest { for p in &self.payload { packet.write_u16::(p.len() as u16).unwrap(); - packet.write_all(p).unwrap(); + packet.write(p).unwrap(); } packet diff --git a/core/src/packet.rs b/core/src/packet.rs deleted file mode 100644 index de780f13..00000000 --- a/core/src/packet.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Ported from librespot-java. Relicensed under MIT with permission. - -use num_derive::{FromPrimitive, ToPrimitive}; - -#[derive(Debug, FromPrimitive, ToPrimitive)] -pub enum PacketType { - SecretBlock = 0x02, - Ping = 0x04, - StreamChunk = 0x08, - StreamChunkRes = 0x09, - ChannelError = 0x0a, - ChannelAbort = 0x0b, - RequestKey = 0x0c, - AesKey = 0x0d, - AesKeyError = 0x0e, - Image = 0x19, - CountryCode = 0x1b, - Pong = 0x49, - PongAck = 0x4a, - Pause = 0x4b, - ProductInfo = 0x50, - LegacyWelcome = 0x69, - LicenseVersion = 0x76, - Login = 0xab, - APWelcome = 0xac, - AuthFailure = 0xad, - MercuryReq = 0xb2, - MercurySub = 0xb3, - MercuryUnsub = 0xb4, - MercuryEvent = 0xb5, - TrackEndedTime = 0x82, - UnknownDataAllZeros = 0x1f, - PreferredLocale = 0x74, - Unknown0x0f = 0x0f, - Unknown0x10 = 0x10, - Unknown0x4f = 0x4f, - - // TODO - occurs when subscribing with an empty URI. Maybe a MercuryError? - // Payload: b"\0\x08\0\0\0\0\0\0\0\0\x01\0\x01\0\x03 \xb0\x06" - Unknown0xb6 = 0xb6, -} diff --git a/core/src/session.rs b/core/src/session.rs index 81975a80..17452b20 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -11,23 +11,19 @@ use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use futures_core::TryStream; use futures_util::{future, ready, StreamExt, TryStreamExt}; -use num_traits::FromPrimitive; use once_cell::sync::OnceCell; use thiserror::Error; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; -use crate::apresolve::ApResolver; +use crate::apresolve::apresolve; 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::token::TokenProvider; #[derive(Debug, Error)] pub enum SessionError { @@ -48,14 +44,11 @@ struct SessionInternal { config: SessionConfig, data: RwLock, - http_client: HttpClient, tx_connection: mpsc::UnboundedSender<(u8, Vec)>, - apresolver: OnceCell, audio_key: OnceCell, channel: OnceCell, mercury: OnceCell, - token_provider: OnceCell, cache: Option>, handle: tokio::runtime::Handle, @@ -74,7 +67,40 @@ impl Session { credentials: Credentials, cache: Option, ) -> Result { - let http_client = HttpClient::new(config.proxy.as_ref()); + let ap = apresolve(config.proxy.as_ref(), config.ap_port) + .await + .accesspoint; + + info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); + let mut conn = connection::connect(&ap.0, ap.1, config.proxy.as_ref()).await?; + + let reusable_credentials = + connection::authenticate(&mut conn, credentials, &config.device_id).await?; + info!("Authenticated as \"{}\" !", reusable_credentials.username); + if let Some(cache) = &cache { + cache.save_credentials(&reusable_credentials); + } + + let session = Session::create( + conn, + config, + cache, + reusable_credentials.username, + tokio::runtime::Handle::current(), + ); + + Ok(session) + } + + fn create( + transport: connection::Transport, + config: SessionConfig, + cache: Option, + username: String, + handle: tokio::runtime::Handle, + ) -> Session { + let (sink, stream) = transport.split(); + let (sender_tx, sender_rx) = mpsc::unbounded_channel(); let session_id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed); @@ -84,37 +110,19 @@ impl Session { config, data: RwLock::new(SessionData { country: String::new(), - canonical_username: String::new(), + canonical_username: username, invalid: false, time_delta: 0, }), - http_client, tx_connection: sender_tx, cache: cache.map(Arc::new), - apresolver: OnceCell::new(), audio_key: OnceCell::new(), channel: OnceCell::new(), mercury: OnceCell::new(), - token_provider: OnceCell::new(), - handle: tokio::runtime::Handle::current(), + handle, session_id, })); - let ap = session.apresolver().resolve("accesspoint").await; - info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); - let mut transport = - connection::connect(&ap.0, ap.1, session.config().proxy.as_ref()).await?; - - let reusable_credentials = - 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(); - if let Some(cache) = session.cache() { - cache.save_credentials(&reusable_credentials); - } - - let (sink, stream) = transport.split(); let sender_task = UnboundedReceiverStream::new(sender_rx) .map(Ok) .forward(sink); @@ -128,13 +136,7 @@ impl Session { } }); - Ok(session) - } - - pub fn apresolver(&self) -> &ApResolver { - self.0 - .apresolver - .get_or_init(|| ApResolver::new(self.weak())) + session } pub fn audio_key(&self) -> &AudioKeyManager { @@ -149,22 +151,12 @@ impl Session { .get_or_init(|| ChannelManager::new(self.weak())) } - pub fn http_client(&self) -> &HttpClient { - &self.0.http_client - } - pub fn mercury(&self) -> &MercuryManager { self.0 .mercury .get_or_init(|| MercuryManager::new(self.weak())) } - pub fn token_provider(&self) -> &TokenProvider { - self.0 - .token_provider - .get_or_init(|| TokenProvider::new(self.weak())) - } - pub fn time_delta(&self) -> i64 { self.0.data.read().unwrap().time_delta } @@ -186,11 +178,10 @@ impl Session { ); } + #[allow(clippy::match_same_arms)] fn dispatch(&self, cmd: u8, data: Bytes) { - use PacketType::*; - let packet_type = FromPrimitive::from_u8(cmd); - match packet_type { - Some(Ping) => { + match cmd { + 0x4 => { let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64; let timestamp = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(dur) => dur, @@ -201,47 +192,31 @@ 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(0x49, vec![0, 0, 0, 0]); } - Some(CountryCode) => { + 0x4a => (), + 0x1b => { let country = String::from_utf8(data.as_ref().to_owned()).unwrap(); info!("Country: {:?}", country); self.0.data.write().unwrap().country = country; } - Some(StreamChunkRes) | Some(ChannelError) => { - self.channel().dispatch(packet_type.unwrap(), data); - } - Some(AesKey) | Some(AesKeyError) => { - self.audio_key().dispatch(packet_type.unwrap(), data); - } - Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => { - self.mercury().dispatch(packet_type.unwrap(), data); - } - Some(PongAck) - | Some(SecretBlock) - | Some(LegacyWelcome) - | Some(UnknownDataAllZeros) - | Some(ProductInfo) - | Some(LicenseVersion) => {} - _ => { - if let Some(packet_type) = PacketType::from_u8(cmd) { - trace!("Ignoring {:?} packet with data {:?}", packet_type, data); - } else { - trace!("Ignoring unknown packet {:x}", cmd); - } - } + + 0x9 | 0xa => self.channel().dispatch(cmd, data), + 0xd | 0xe => self.audio_key().dispatch(cmd, data), + 0xb2..=0xb6 => self.mercury().dispatch(cmd, data), + _ => (), } } - 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: u8, data: Vec) { + self.0.tx_connection.send((cmd, data)).unwrap(); } pub fn cache(&self) -> Option<&Arc> { self.0.cache.as_ref() } - pub fn config(&self) -> &SessionConfig { + fn config(&self) -> &SessionConfig { &self.0.config } diff --git a/core/src/spclient.rs b/core/src/spclient.rs deleted file mode 100644 index eb7b3f0f..00000000 --- a/core/src/spclient.rs +++ /dev/null @@ -1 +0,0 @@ -// https://github.com/librespot-org/librespot-java/blob/27783e06f456f95228c5ac37acf2bff8c1a8a0c4/lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index e6e2bae0..3372572a 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -116,25 +116,22 @@ impl SpotifyId { /// /// [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)?; - - if src.len() <= SpotifyId::SIZE_BASE62 { + // We expect the ID to be the last colon-delimited item in the URI. + let b = src.as_bytes(); + let id_i = b.len() - SpotifyId::SIZE_BASE62; + if b[id_i - 1] != b':' { return Err(SpotifyIdError); } - let colon_index = src.len() - SpotifyId::SIZE_BASE62 - 1; + let mut id = SpotifyId::from_base62(&src[id_i..])?; - if src.as_bytes()[colon_index] != b':' { - return Err(SpotifyIdError); - } - - let mut id = SpotifyId::from_base62(&src[colon_index + 1..])?; - id.audio_type = src[..colon_index].into(); + // Slice offset by 8 as we are skipping the "spotify:" prefix. + id.audio_type = src[8..id_i - 1].into(); Ok(id) } - /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32) + /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE62` (22) /// character long `String`. pub fn to_base16(&self) -> String { to_base16(&self.to_raw(), &mut [0u8; SpotifyId::SIZE_BASE16]) @@ -308,7 +305,7 @@ mod tests { }, ]; - static CONV_INVALID: [ConversionCase; 3] = [ + static CONV_INVALID: [ConversionCase; 2] = [ ConversionCase { id: 0, kind: SpotifyAudioType::NonPlayable, @@ -333,18 +330,6 @@ mod tests { 154, 27, 28, 251, ], }, - ConversionCase { - id: 0, - kind: SpotifyAudioType::NonPlayable, - // Uri too short - uri: "spotify:azb:aRS48xBl0tH", - base16: "--------------------", - base62: "....................", - raw: &[ - // Invalid length. - 154, 27, 28, 251, - ], - }, ]; #[test] diff --git a/core/src/token.rs b/core/src/token.rs deleted file mode 100644 index 824fcc3b..00000000 --- a/core/src/token.rs +++ /dev/null @@ -1,131 +0,0 @@ -// Ported from librespot-java. Relicensed under MIT with permission. - -// Known scopes: -// ugc-image-upload, playlist-read-collaborative, playlist-modify-private, -// playlist-modify-public, playlist-read-private, user-read-playback-position, -// user-read-recently-played, user-top-read, user-modify-playback-state, -// user-read-currently-playing, user-read-playback-state, user-read-private, user-read-email, -// user-library-modify, user-library-read, user-follow-modify, user-follow-read, streaming, -// app-remote-control - -use crate::mercury::MercuryError; - -use serde::Deserialize; - -use std::error::Error; -use std::time::{Duration, Instant}; - -component! { - TokenProvider : TokenProviderInner { - tokens: Vec = vec![], - } -} - -#[derive(Clone, Debug)] -pub struct Token { - access_token: String, - expires_in: Duration, - token_type: String, - scopes: Vec, - timestamp: Instant, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct TokenData { - access_token: String, - expires_in: u64, - token_type: String, - scope: Vec, -} - -impl TokenProvider { - const KEYMASTER_CLIENT_ID: &'static str = "65b708073fc0480ea92a077233ca87bd"; - - fn find_token(&self, scopes: Vec<&str>) -> Option { - self.lock(|inner| { - for i in 0..inner.tokens.len() { - if inner.tokens[i].in_scopes(scopes.clone()) { - return Some(i); - } - } - None - }) - } - - // scopes must be comma-separated - pub async fn get_token(&self, scopes: &str) -> Result { - if scopes.is_empty() { - return Err(MercuryError); - } - - 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() { - self.lock(|inner| inner.tokens.remove(index)); - } else { - return Ok(cached_token); - } - } - - trace!( - "Requested token in scopes {:?} unavailable or expired, requesting new token.", - scopes - ); - - let query_uri = format!( - "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}", - scopes, - Self::KEYMASTER_CLIENT_ID, - self.session().device_id() - ); - 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)?; - trace!("Got token: {:?}", token); - self.lock(|inner| inner.tokens.push(token.clone())); - Ok(token) - } -} - -impl Token { - const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10); - - pub fn new(body: String) -> Result> { - let data: TokenData = serde_json::from_slice(body.as_ref())?; - Ok(Self { - access_token: data.access_token, - expires_in: Duration::from_secs(data.expires_in), - token_type: data.token_type, - scopes: data.scope, - timestamp: Instant::now(), - }) - } - - pub fn is_expired(&self) -> bool { - self.timestamp + (self.expires_in - Self::EXPIRY_THRESHOLD) < Instant::now() - } - - pub fn in_scope(&self, scope: &str) -> bool { - for s in &self.scopes { - if *s == scope { - return true; - } - } - false - } - - pub fn in_scopes(&self, scopes: Vec<&str>) -> bool { - for s in scopes { - if !self.in_scope(s) { - return false; - } - } - true - } -} diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml deleted file mode 100644 index 9ea9df48..00000000 --- a/discovery/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "librespot-discovery" -version = "0.2.0" -authors = ["Paul Lietar "] -description = "The discovery logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2018" - -[dependencies] -aes-ctr = "0.6" -base64 = "0.13" -cfg-if = "1.0" -form_urlencoded = "1.0" -futures-core = "0.3" -hmac = "0.11" -hyper = { version = "0.14", features = ["server", "http1", "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"] } - -dns-sd = { version = "0.1.3", optional = true } - -[dependencies.librespot-core] -path = "../core" -default_features = false -version = "0.2.0" - -[dev-dependencies] -futures = "0.3" -hex = "0.4" -simple_logger = "1.11" -tokio = { version = "1.0", features = ["macros", "rt"] } - -[features] -with-dns-sd = ["dns-sd"] diff --git a/discovery/examples/discovery.rs b/discovery/examples/discovery.rs deleted file mode 100644 index cd913fd2..00000000 --- a/discovery/examples/discovery.rs +++ /dev/null @@ -1,25 +0,0 @@ -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())); - - let mut server = librespot_discovery::Discovery::builder(device_id) - .name(name) - .device_type(DeviceType::Computer) - .launch() - .unwrap(); - - while let Some(x) = server.next().await { - println!("Received {:?}", x); - } -} diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs deleted file mode 100644 index b1249a0d..00000000 --- a/discovery/src/lib.rs +++ /dev/null @@ -1,150 +0,0 @@ -//! Advertises this device to Spotify clients in the local network. -//! -//! This device will show up in the list of "available devices". -//! Once it is selected from the list, [`Credentials`] are received. -//! Those can be used to establish a new Session with [`librespot_core`]. -//! -//! 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; -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; - -use cfg_if::cfg_if; -use futures_core::Stream; -use librespot_core as core; -use thiserror::Error; - -use self::server::DiscoveryServer; - -/// Credentials to be used in [`librespot`](`librespot_core`). -pub use crate::core::authentication::Credentials; - -/// Determining the icon in the list of available devices. -pub use crate::core::config::DeviceType; - -/// Makes this device visible to Spotify clients in the local network. -/// -/// `Discovery` implements the [`Stream`] trait. Every time this device -/// is selected in the list of available devices, it yields [`Credentials`]. -pub struct Discovery { - server: DiscoveryServer, - - #[cfg(not(feature = "with-dns-sd"))] - _svc: libmdns::Service, - #[cfg(feature = "with-dns-sd")] - _svc: dns_sd::DNSService, -} - -/// A builder for [`Discovery`]. -pub struct Builder { - server_config: server::Config, - port: u16, -} - -/// Errors that can occur while setting up a [`Discovery`] instance. -#[derive(Debug, Error)] -pub enum Error { - /// 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("Setting up the http server failed: {0}")] - HttpServerError(#[from] hyper::Error), -} - -impl Builder { - /// Starts a new builder using the provided device id. - pub fn new(device_id: impl Into) -> Self { - Self { - server_config: server::Config { - name: "Librespot".into(), - device_type: DeviceType::default(), - device_id: device_id.into(), - }, - port: 0, - } - } - - /// Sets the name to be displayed. Default is `"Librespot"`. - pub fn name(mut self, name: impl Into>) -> Self { - self.server_config.name = name.into(); - self - } - - /// Sets the device type which is visible as icon in other Spotify clients. Default is `Speaker`. - pub fn device_type(mut self, device_type: DeviceType) -> Self { - self.server_config.device_type = device_type; - self - } - - /// Sets the port on which it should listen to incoming connections. - /// The default value `0` means any port. - pub fn port(mut self, port: u16) -> Self { - self.port = port; - self - } - - /// Sets up the [`Discovery`] instance. - /// - /// # Errors - /// If setting up the mdns service or creating the server fails, this function returns an error. - 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 svc; - - cfg_if! { - if #[cfg(feature = "with-dns-sd")] { - svc = dns_sd::DNSService::register( - Some(name.as_ref()), - "_spotify-connect._tcp", - None, - None, - port, - &["VERSION=1.0", "CPath=/"], - ) - .unwrap(); - - } else { - let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; - svc = responder.register( - "_spotify-connect._tcp".to_owned(), - name, - port, - &["VERSION=1.0", "CPath=/"], - ) - } - }; - - Ok(Discovery { server, _svc: svc }) - } -} - -impl Discovery { - /// Starts a [`Builder`] with the provided device id. - pub fn builder(device_id: impl Into) -> Builder { - Builder::new(device_id) - } - - /// Create a new instance with the specified device id and default paramaters. - pub fn new(device_id: impl Into) -> Result { - Self::builder(device_id).launch() - } -} - -impl Stream for Discovery { - type Item = Credentials; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.server).poll_next(cx) - } -} diff --git a/discovery/src/server.rs b/discovery/src/server.rs deleted file mode 100644 index 53b849f7..00000000 --- a/discovery/src/server.rs +++ /dev/null @@ -1,236 +0,0 @@ -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 aes_ctr::cipher::generic_array::GenericArray; -use aes_ctr::cipher::{NewStreamCipher, SyncStreamCipher}; -use aes_ctr::Aes128Ctr; -use futures_core::Stream; -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 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; - -type Params<'a> = BTreeMap, Cow<'a, str>>; - -pub struct Config { - pub name: Cow<'static, str>, - pub device_type: DeviceType, - pub device_id: String, -} - -struct RequestHandler { - config: Config, - keys: DhLocalKeys, - tx: mpsc::UnboundedSender, -} - -impl RequestHandler { - fn new(config: Config) -> (Self, mpsc::UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded_channel(); - - let discovery = Self { - config, - keys: DhLocalKeys::random(&mut rand::thread_rng()), - tx, - }; - - (discovery, rx) - } - - fn handle_get_info(&self) -> Response { - let public_key = base64::encode(&self.keys.public_key()); - let device_type: &str = self.config.device_type.into(); - - let body = json!({ - "status": 101, - "statusString": "ERROR-OK", - "spotifyError": 0, - "version": "2.7.1", - "deviceID": (self.config.device_id), - "remoteName": (self.config.name), - "activeUser": "", - "publicKey": (public_key), - "deviceType": (device_type), - "libraryVersion": crate::core::version::SEMVER, - "accountReq": "PREMIUM", - "brandDisplayName": "librespot", - "modelDisplayName": "librespot", - "resolverVersion": "0", - "groupStatus": "NONE", - "voiceSupport": "NO", - }) - .to_string(); - - 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(); - - let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); - - let client_key = base64::decode(client_key.as_bytes()).unwrap(); - let shared_key = self.keys.shared_secret(&client_key); - - let iv = &encrypted_blob[0..16]; - let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; - let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; - - let base_key = Sha1::digest(&shared_key); - 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"); - 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"); - 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"); - h.update(encrypted); - if h.verify(cksum).is_err() { - warn!("Login error for user {:?}: MAC mismatch", username); - let result = json!({ - "status": 102, - "spotifyError": 1, - "statusString": "ERROR-MAC" - }); - - let body = result.to_string(); - return Response::new(Body::from(body)); - } - - let decrypted = { - let mut data = encrypted.to_vec(); - let mut cipher = Aes128Ctr::new( - GenericArray::from_slice(&encryption_key[0..16]), - GenericArray::from_slice(iv), - ); - cipher.apply_keystream(&mut data); - String::from_utf8(data).unwrap() - }; - - let credentials = - Credentials::with_blob(username.to_string(), &decrypted, &self.config.device_id); - - self.tx.send(credentials).unwrap(); - - let result = json!({ - "status": 101, - "spotifyError": 0, - "statusString": "ERROR-OK" - }); - - let body = result.to_string(); - Response::new(Body::from(body)) - } - - fn not_found(&self) -> Response { - let mut res = Response::default(); - *res.status_mut() = StatusCode::NOT_FOUND; - res - } - - async fn handle(self: Arc, request: Request) -> hyper::Result> { - let mut params = Params::new(); - - let (parts, body) = request.into_parts(); - - if let Some(query) = parts.uri.query() { - let query_params = form_urlencoded::parse(query.as_bytes()); - params.extend(query_params); - } - - if parts.method != Method::GET { - debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params); - } - - let body = hyper::body::to_bytes(body).await?; - - params.extend(form_urlencoded::parse(&body)); - - let action = params.get("action").map(Cow::as_ref); - - Ok(match (parts.method, action) { - (Method::GET, Some("getInfo")) => self.handle_get_info(), - (Method::POST, Some("addUser")) => self.handle_add_user(¶ms), - _ => self.not_found(), - }) - } -} - -pub struct DiscoveryServer { - cred_rx: mpsc::UnboundedReceiver, - _close_tx: oneshot::Sender, -} - -impl DiscoveryServer { - pub fn new(config: Config, port: &mut u16) -> hyper::Result { - let (discovery, cred_rx) = RequestHandler::new(config); - let discovery = Arc::new(discovery); - - let (close_tx, close_rx) = oneshot::channel(); - - let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port); - - let make_service = make_service_fn(move |_| { - let discovery = discovery.clone(); - async move { - Ok::<_, hyper::Error>(service_fn(move |request| discovery.clone().handle(request))) - } - }); - - let server = hyper::Server::try_bind(&address)?.serve(make_service); - - *port = server.local_addr().port(); - debug!("Zeroconf server listening on 0.0.0.0:{}", *port); - - tokio::spawn(async { - let result = server - .with_graceful_shutdown(async { - close_rx.await.unwrap_err(); - debug!("Shutting down discovery server"); - }) - .await; - - if let Err(e) = result { - warn!("Discovery server failed: {}", e); - } - }); - - Ok(Self { - cred_rx, - _close_tx: close_tx, - }) - } -} - -impl Stream for DiscoveryServer { - type Item = Credentials; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.cred_rx.poll_recv(cx) - } -} diff --git a/examples/get_token.rs b/examples/get_token.rs index 3ef6bd71..636155e0 100644 --- a/examples/get_token.rs +++ b/examples/get_token.rs @@ -2,6 +2,7 @@ use std::env; use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; +use librespot::core::keymaster; use librespot::core::session::Session; const SCOPES: &str = @@ -12,8 +13,8 @@ async fn main() { let session_config = SessionConfig::default(); let args: Vec<_> = env::args().collect(); - if args.len() != 3 { - eprintln!("Usage: {} USERNAME PASSWORD", args[0]); + if args.len() != 4 { + eprintln!("Usage: {} USERNAME PASSWORD CLIENT_ID", args[0]); return; } @@ -25,6 +26,8 @@ async fn main() { println!( "Token: {:#?}", - session.token_provider().get_token(SCOPES).await.unwrap() + keymaster::get_token(&session, &args[3], SCOPES) + .await + .unwrap() ); } diff --git a/metadata/src/cover.rs b/metadata/src/cover.rs index b483f454..408e658e 100644 --- a/metadata/src/cover.rs +++ b/metadata/src/cover.rs @@ -2,7 +2,6 @@ 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; @@ -14,7 +13,7 @@ pub fn get(session: &Session, file: FileId) -> ChannelData { packet.write_u16::(channel_id).unwrap(); packet.write_u16::(0).unwrap(); packet.write(&file.0).unwrap(); - session.send_packet(PacketType::Image, packet); + session.send_packet(0x19, packet); data } diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 0bed793c..37806062 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -18,15 +18,15 @@ path = "../metadata" version = "0.2.0" [dependencies] +cfg-if = "1.0" futures-executor = "0.3" 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"] } -zerocopy = { version = "0.3" } +zerocopy = { version = "0.3" } -# Backends alsa = { version = "0.5", optional = true } portaudio-rs = { version = "0.3", optional = true } libpulse-binding = { version = "2", optional = true, default-features = false } @@ -42,16 +42,14 @@ rodio = { version = "0.14", optional = true, default-features = false cpal = { version = "0.13", optional = true } thiserror = { version = "1", optional = true } -# Decoder -lewton = "0.10" +# Decoders +lewton = "0.10" # Currently not optional because of limitations of cargo features +librespot-tremor = { version = "0.2", optional = true } ogg = "0.8" - -# Dithering -rand = "0.8" -rand_distr = "0.4" +vorbis = { version ="0.0", optional = true } [features] -alsa-backend = ["alsa", "thiserror"] +alsa-backend = ["alsa"] portaudio-backend = ["portaudio-rs"] pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] jackaudio-backend = ["jack"] @@ -59,3 +57,6 @@ rodio-backend = ["rodio", "cpal", "thiserror"] rodiojack-backend = ["rodio", "cpal/jack", "thiserror"] sdl-backend = ["sdl2"] gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] + +with-tremor = ["librespot-tremor"] +with-vorbis = ["vorbis"] \ No newline at end of file diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 7101f96d..c7bc4e55 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,189 +1,95 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; -use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; use alsa::device_name::HintIter; -use alsa::pcm::{Access, Format, HwParams, PCM}; -use alsa::{Direction, ValueOr}; +use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; +use alsa::{Direction, Error, ValueOr}; use std::cmp::min; +use std::ffi::CString; use std::io; 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; - -#[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}")] - UnsupportedFormat { - device: String, - format: AudioFormat, - err: 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}")] - UnsupportedChannelCount { - device: String, - channel_count: u8, - err: alsa::Error, - }, - #[error("AlsaSink Hardware Parameters Error, {0}")] - HwParams(alsa::Error), - #[error("AlsaSink Software Parameters Error, {0}")] - SwParams(alsa::Error), - #[error("AlsaSink PCM Error, {0}")] - Pcm(alsa::Error), -} +const BUFFERED_LATENCY: f32 = 0.125; // seconds +const BUFFERED_PERIODS: Frames = 4; pub struct AlsaSink { pcm: Option, format: AudioFormat, device: String, - period_buffer: Vec, + buffer: Vec, } -fn list_outputs() -> io::Result<()> { - println!("Listing available Alsa outputs:"); +fn list_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(None, &*CString::new(*t).unwrap()).unwrap(); 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"))?; - println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t")); + println!( + "{}\n\t{}\n", + a.name.unwrap(), + a.desc.unwrap().replace("\n", "\n\t") + ); } } } - - Ok(()) } -fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), AlsaError> { - let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp { - device: dev_name.to_string(), - err: e, - })?; - +fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box> { + let pcm = PCM::new(dev_name, Direction::Playback, false)?; 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, + AudioFormat::S16 => Format::s16(), }; - 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, - })?; + // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8 + // latency = period_size * periods / (rate * bytes_per_frame) + // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. + let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32 + * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames; + { + let hwp = HwParams::any(&pcm)?; + hwp.set_access(Access::RWInterleaved)?; + hwp.set_format(alsa_format)?; + hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?; + hwp.set_channels(NUM_CHANNELS as u32)?; + period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?; + hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?; + pcm.hw_params(&hwp)?; - hwp.set_format(alsa_format) - .map_err(|e| AlsaError::UnsupportedFormat { - device: dev_name.to_string(), - format, - err: e, - })?; + let swp = pcm.sw_params_current()?; + swp.set_start_threshold(hwp.get_buffer_size()? - hwp.get_period_size()?)?; + pcm.sw_params(&swp)?; + } - hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| { - AlsaError::UnsupportedSampleRate { - device: dev_name.to_string(), - samplerate: SAMPLE_RATE, - err: e, - } - })?; - - hwp.set_channels(NUM_CHANNELS as u32) - .map_err(|e| AlsaError::UnsupportedChannelCount { - device: dev_name.to_string(), - channel_count: NUM_CHANNELS, - err: e, - })?; - - // Deal strictly in time and periods. - hwp.set_periods(NUM_PERIODS, ValueOr::Nearest) - .map_err(AlsaError::HwParams)?; - - hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest) - .map_err(AlsaError::HwParams)?; - - pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; - - let swp = pcm.sw_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)?; - - swp.set_start_threshold(frames_per_buffer - frames_per_period) - .map_err(AlsaError::SwParams)?; - - pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; - - // Let ALSA do the math for us. - pcm.frames_to_bytes(frames_per_period) as usize - }; - - Ok((pcm, bytes_per_period)) + Ok((pcm, period_size)) } impl Open for AlsaSink { fn open(device: Option, format: AudioFormat) -> Self { - let name = match device.as_deref() { - Some("?") => match list_outputs() { - Ok(_) => { - exit(0); - } - Err(err) => { - error!("Error listing Alsa outputs, {}", err); - exit(1); - } - }, + info!("Using Alsa sink with format: {:?}", format); + + let name = match device.as_ref().map(AsRef::as_ref) { + Some("?") => { + println!("Listing available Alsa outputs:"); + list_outputs(); + exit(0) + } Some(device) => device, None => "default", } .to_string(); - info!("Using AlsaSink with format: {:?}", format); - Self { pcm: None, format, device: name, - period_buffer: vec![], + buffer: vec![], } } } @@ -191,13 +97,21 @@ impl Open for AlsaSink { impl Sink for AlsaSink { fn start(&mut self) -> io::Result<()> { if self.pcm.is_none() { - 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); + let pcm = open_device(&self.device, self.format); + match pcm { + Ok((p, period_size)) => { + self.pcm = Some(p); + // Create a buffer for all samples for a full period + self.buffer = Vec::with_capacity( + period_size as usize * BUFFERED_PERIODS as usize * self.format.size(), + ); } Err(e) => { - return Err(io::Error::new(io::ErrorKind::Other, e)); + error!("Alsa error PCM open {}", e); + return Err(io::Error::new( + io::ErrorKind::Other, + "Alsa error: PCM open failed", + )); } } } @@ -209,16 +123,9 @@ impl Sink for AlsaSink { { // 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), - ) - })? + self.write_bytes(&[]).expect("could not flush buffer"); + let pcm = self.pcm.as_mut().unwrap(); + pcm.drain().unwrap(); } self.pcm = None; Ok(()) @@ -232,15 +139,15 @@ impl SinkAsBytes for AlsaSink { let mut processed_data = 0; while processed_data < data.len() { let data_to_buffer = min( - self.period_buffer.capacity() - self.period_buffer.len(), + self.buffer.capacity() - self.buffer.len(), data.len() - processed_data, ); - self.period_buffer + self.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() { - self.write_buf()?; - self.period_buffer.clear(); + if self.buffer.len() == self.buffer.capacity() { + self.write_buf(); + self.buffer.clear(); } } @@ -249,34 +156,12 @@ 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", - ) - })?; + fn write_buf(&mut self) { + let pcm = self.pcm.as_mut().unwrap(); let io = pcm.io_bytes(); - if let Err(err) = io.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 - ); - 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 - ), - ) - })? - } - - Ok(()) + match io.writei(&self.buffer) { + Ok(_) => (), + Err(err) => pcm.try_recover(err, false).unwrap(), + }; } } diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 58f6cbc9..e31c66ae 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,8 +1,7 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; -use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use gstreamer as gst; use gstreamer_app as gst_app; @@ -34,17 +33,11 @@ impl Open for GstreamerSink { let sample_size = format.size(); let gst_bytes = 2048 * sample_size; - #[cfg(target_endian = "little")] - const ENDIANNESS: &str = "LE"; - #[cfg(target_endian = "big")] - const ENDIANNESS: &str = "BE"; - let pipeline_str_preamble = format!( - "appsrc caps=\"audio/x-raw,format={}{},layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ", - gst_format, ENDIANNESS, NUM_CHANNELS, SAMPLE_RATE, gst_bytes + "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ", + gst_format, NUM_CHANNELS, SAMPLE_RATE, gst_bytes ); - // no need to dither twice; use librespot dithering instead - let pipeline_str_rest = r#" ! audioconvert dithering=none ! autoaudiosink"#; + let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { Some(x) => format!("{}{}", pipeline_str_preamble, x), None => format!("{}{}", pipeline_str_preamble, pipeline_str_rest), @@ -127,6 +120,7 @@ impl Open for GstreamerSink { } impl Sink for GstreamerSink { + start_stop_noop!(); sink_as_bytes!(); } @@ -139,7 +133,3 @@ impl SinkAsBytes for GstreamerSink { Ok(()) } } - -impl GstreamerSink { - pub const NAME: &'static str = "gstreamer"; -} diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index f55f20a8..816147ff 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,8 +1,7 @@ use super::{Open, Sink}; use crate::config::AudioFormat; -use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::NUM_CHANNELS; +use crate::player::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; @@ -70,10 +69,11 @@ 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()); - for sample in samples_f32.iter() { - let res = self.send.send(*sample); + start_stop_noop!(); + + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + for s in packet.samples().iter() { + let res = self.send.send(*s); if res.is_err() { error!("cannot write to channel"); } @@ -81,7 +81,3 @@ impl Sink for JackSink { Ok(()) } } - -impl JackSink { - pub const NAME: &'static str = "jackaudio"; -} diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 31fb847c..84e35634 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,5 +1,4 @@ use crate::config::AudioFormat; -use crate::convert::Converter; use crate::decoder::AudioPacket; use std::io; @@ -8,13 +7,9 @@ pub trait Open { } pub trait Sink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()>; + fn start(&mut self) -> io::Result<()>; + fn stop(&mut self) -> io::Result<()>; + fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; } pub type SinkBuilder = fn(Option, AudioFormat) -> Box; @@ -30,30 +25,26 @@ 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<()> { - use crate::convert::i24; + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + use crate::convert::{self, i24}; use zerocopy::AsBytes; match packet { AudioPacket::Samples(samples) => match self.format { - AudioFormat::F64 => self.write_bytes(samples.as_bytes()), - AudioFormat::F32 => { - let samples_f32: &[f32] = &converter.f64_to_f32(samples); - self.write_bytes(samples_f32.as_bytes()) - } + AudioFormat::F32 => self.write_bytes(samples.as_bytes()), AudioFormat::S32 => { - let samples_s32: &[i32] = &converter.f64_to_s32(samples); + let samples_s32: &[i32] = &convert::to_s32(samples); self.write_bytes(samples_s32.as_bytes()) } AudioFormat::S24 => { - let samples_s24: &[i32] = &converter.f64_to_s24(samples); + let samples_s24: &[i32] = &convert::to_s24(samples); self.write_bytes(samples_s24.as_bytes()) } AudioFormat::S24_3 => { - let samples_s24_3: &[i24] = &converter.f64_to_s24_3(samples); + let samples_s24_3: &[i24] = &convert::to_s24_3(samples); self.write_bytes(samples_s24_3.as_bytes()) } AudioFormat::S16 => { - let samples_s16: &[i16] = &converter.f64_to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); self.write_bytes(samples_s16.as_bytes()) } }, @@ -63,6 +54,17 @@ macro_rules! sink_as_bytes { }; } +macro_rules! start_stop_noop { + () => { + fn start(&mut self) -> io::Result<()> { + Ok(()) + } + fn stop(&mut self) -> io::Result<()> { + Ok(()) + } + }; +} + #[cfg(feature = "alsa-backend")] mod alsa; #[cfg(feature = "alsa-backend")] @@ -90,8 +92,6 @@ use self::gstreamer::GstreamerSink; #[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] mod rodio; -#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] -use self::rodio::RodioSink; #[cfg(feature = "sdl-backend")] mod sdl; @@ -105,24 +105,24 @@ mod subprocess; use self::subprocess::SubprocessSink; pub const BACKENDS: &[(&str, SinkBuilder)] = &[ - #[cfg(feature = "rodio-backend")] - (RodioSink::NAME, rodio::mk_rodio), // default goes first #[cfg(feature = "alsa-backend")] - (AlsaSink::NAME, mk_sink::), + ("alsa", mk_sink::), #[cfg(feature = "portaudio-backend")] - (PortAudioSink::NAME, mk_sink::), + ("portaudio", mk_sink::), #[cfg(feature = "pulseaudio-backend")] - (PulseAudioSink::NAME, mk_sink::), + ("pulseaudio", mk_sink::), #[cfg(feature = "jackaudio-backend")] - (JackSink::NAME, mk_sink::), + ("jackaudio", mk_sink::), #[cfg(feature = "gstreamer-backend")] - (GstreamerSink::NAME, mk_sink::), + ("gstreamer", mk_sink::), + #[cfg(feature = "rodio-backend")] + ("rodio", rodio::mk_rodio), #[cfg(feature = "rodiojack-backend")] ("rodiojack", rodio::mk_rodiojack), #[cfg(feature = "sdl-backend")] - (SdlSink::NAME, mk_sink::), - (StdoutSink::NAME, mk_sink::), - (SubprocessSink::NAME, mk_sink::), + ("sdl", mk_sink::), + ("pipe", mk_sink::), + ("subprocess", mk_sink::), ]; pub fn find(name: Option) -> Option { diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 56040384..df3e6c0f 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,66 +1,36 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; -use crate::convert::Converter; use crate::decoder::AudioPacket; use std::fs::OpenOptions; use std::io::{self, Write}; pub struct StdoutSink { - output: Option>, - path: Option, + output: Box, format: AudioFormat, } impl Open for StdoutSink { fn open(path: Option, format: AudioFormat) -> Self { info!("Using pipe sink with format: {:?}", format); - Self { - output: None, - path, - format, - } + + let output: Box = match path { + Some(path) => Box::new(OpenOptions::new().write(true).open(path).unwrap()), + _ => Box::new(io::stdout()), + }; + + Self { output, format } } } impl Sink for StdoutSink { - fn start(&mut self) -> io::Result<()> { - 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))?; - Box::new(open_op) - } - None => Box::new(io::stdout()), - }; - - self.output = Some(output); - } - - Ok(()) - } - + start_stop_noop!(); sink_as_bytes!(); } impl SinkAsBytes for StdoutSink { fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { - match self.output.as_deref_mut() { - Some(output) => { - output.write_all(data)?; - output.flush()?; - } - None => { - return Err(io::Error::new(io::ErrorKind::Other, "Output is None")); - } - } - + self.output.write_all(data)?; + self.output.flush()?; Ok(()) } } - -impl StdoutSink { - pub const NAME: &'static str = "pipe"; -} diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 378deb48..4fe471a9 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,8 +1,8 @@ use super::{Open, Sink}; use crate::config::AudioFormat; -use crate::convert::Converter; +use crate::convert; use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; use std::io; @@ -55,9 +55,12 @@ impl<'a> Open for PortAudioSink<'a> { fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { info!("Using PortAudio sink with format: {:?}", format); + warn!("This backend is known to panic on several platforms."); + warn!("Consider using some other backend, or better yet, contributing a fix."); + portaudio_rs::initialize().unwrap(); - let device_idx = match device.as_deref() { + let device_idx = match device.as_ref().map(AsRef::as_ref) { Some("?") => { list_outputs(); exit(0) @@ -106,7 +109,7 @@ impl<'a> Sink for PortAudioSink<'a> { Some(*$parameters), SAMPLE_RATE as f64, FRAMES_PER_BUFFER_UNSPECIFIED, - StreamFlags::DITHER_OFF, // no need to dither twice; use librespot dithering instead + StreamFlags::empty(), None, ) .unwrap(), @@ -133,15 +136,15 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - Self::F32(stream, _) => stop_sink!(ref mut stream), - Self::S32(stream, _) => stop_sink!(ref mut stream), - Self::S16(stream, _) => stop_sink!(ref mut stream), + Self::F32(stream, _parameters) => stop_sink!(ref mut stream), + Self::S32(stream, _parameters) => stop_sink!(ref mut stream), + Self::S16(stream, _parameters) => stop_sink!(ref mut stream), }; Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { macro_rules! write_sink { (ref mut $stream: expr, $samples: expr) => { $stream.as_mut().unwrap().write($samples) @@ -151,15 +154,14 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet.samples(); let result = match self { Self::F32(stream, _parameters) => { - let samples_f32: &[f32] = &converter.f64_to_f32(samples); - write_sink!(ref mut stream, samples_f32) + write_sink!(ref mut stream, samples) } Self::S32(stream, _parameters) => { - let samples_s32: &[i32] = &converter.f64_to_s32(samples); + let samples_s32: &[i32] = &convert::to_s32(samples); write_sink!(ref mut stream, samples_s32) } Self::S16(stream, _parameters) => { - let samples_s16: &[i16] = &converter.f64_to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); write_sink!(ref mut stream, samples_s16) } }; @@ -178,7 +180,3 @@ impl<'a> Drop for PortAudioSink<'a> { portaudio_rs::terminate().unwrap(); } } - -impl<'a> PortAudioSink<'a> { - pub const NAME: &'static str = "portaudio"; -} diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index e36941ea..90a4a67a 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,8 +1,7 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; -use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_simple_binding::Simple; use std::io; @@ -23,14 +22,11 @@ impl Open for PulseAudioSink { // 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) - } + AudioFormat::F32 => pulse::sample::Format::F32le, + AudioFormat::S32 => pulse::sample::Format::S32le, + AudioFormat::S24 => pulse::sample::Format::S24_32le, + AudioFormat::S24_3 => pulse::sample::Format::S24le, + AudioFormat::S16 => pulse::sample::Format::S16le, }; let ss = pulse::sample::Spec { @@ -55,7 +51,7 @@ impl Sink for PulseAudioSink { return Ok(()); } - let device = self.device.as_deref(); + let device = self.device.as_ref().map(|s| (*s).as_str()); let result = Simple::new( None, // Use the default server. APP_NAME, // Our application's name. @@ -104,7 +100,3 @@ impl SinkAsBytes for PulseAudioSink { } } } - -impl PulseAudioSink { - pub const NAME: &'static str = "pulseaudio"; -} diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 1e999938..9399a309 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,15 +1,14 @@ use std::process::exit; -use std::time::Duration; -use std::{io, thread}; +use std::{io, thread, time}; use cpal::traits::{DeviceTrait, HostTrait}; use thiserror::Error; use super::Sink; use crate::config::AudioFormat; -use crate::convert::Converter; +use crate::convert; use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; #[cfg(all( feature = "rodiojack-backend", @@ -175,20 +174,18 @@ 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<()> { + start_stop_noop!(); + + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { let samples = packet.samples(); match self.format { AudioFormat::F32 => { - let samples_f32: &[f32] = &converter.f64_to_f32(samples); - let source = rodio::buffer::SamplesBuffer::new( - NUM_CHANNELS as u16, - SAMPLE_RATE, - samples_f32, - ); + let source = + rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); self.rodio_sink.append(source); } AudioFormat::S16 => { - let samples_s16: &[i16] = &converter.f64_to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); let source = rodio::buffer::SamplesBuffer::new( NUM_CHANNELS as u16, SAMPLE_RATE, @@ -204,12 +201,8 @@ impl Sink for RodioSink { // 44100 elements --> about 27 chunks while self.rodio_sink.len() > 26 { // sleep and wait for rodio to drain a bit - thread::sleep(Duration::from_millis(10)); + thread::sleep(time::Duration::from_millis(10)); } Ok(()) } } - -impl RodioSink { - pub const NAME: &'static str = "rodio"; -} diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 28d140e8..a3a608d9 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,11 +1,10 @@ use super::{Open, Sink}; use crate::config::AudioFormat; -use crate::convert::Converter; +use crate::convert; use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; -use std::time::Duration; -use std::{io, thread}; +use std::{io, thread, time}; pub enum SdlSink { F32(AudioQueue), @@ -82,12 +81,12 @@ impl Sink for SdlSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { macro_rules! drain_sink { ($queue: expr, $size: expr) => {{ // sleep and wait for sdl thread to drain the queue a bit while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) { - thread::sleep(Duration::from_millis(10)); + thread::sleep(time::Duration::from_millis(10)); } }}; } @@ -95,17 +94,16 @@ impl Sink for SdlSink { let samples = packet.samples(); match self { Self::F32(queue) => { - let samples_f32: &[f32] = &converter.f64_to_f32(samples); drain_sink!(queue, AudioFormat::F32.size()); - queue.queue(samples_f32) + queue.queue(samples) } Self::S32(queue) => { - let samples_s32: &[i32] = &converter.f64_to_s32(samples); + let samples_s32: &[i32] = &convert::to_s32(samples); drain_sink!(queue, AudioFormat::S32.size()); queue.queue(samples_s32) } Self::S16(queue) => { - let samples_s16: &[i16] = &converter.f64_to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); drain_sink!(queue, AudioFormat::S16.size()); queue.queue(samples_s16) } @@ -113,7 +111,3 @@ impl Sink for SdlSink { Ok(()) } } - -impl SdlSink { - pub const NAME: &'static str = "sdl"; -} diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 64f04c88..f493e7a7 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,6 +1,5 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; -use crate::convert::Converter; use crate::decoder::AudioPacket; use shell_words::split; @@ -62,7 +61,3 @@ impl SinkAsBytes for SubprocessSink { Ok(()) } } - -impl SubprocessSink { - pub const NAME: &'static str = "subprocess"; -} diff --git a/playback/src/config.rs b/playback/src/config.rs index 7604f59f..feb1d61e 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,10 +1,9 @@ -use super::player::db_to_ratio; +use super::player::NormalisationData; use crate::convert::i24; -pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; +use std::convert::TryFrom; use std::mem; use std::str::FromStr; -use std::time::Duration; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum Bitrate { @@ -33,7 +32,6 @@ impl Default for Bitrate { #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum AudioFormat { - F64, F32, S32, S24, @@ -41,11 +39,10 @@ pub enum AudioFormat { S16, } -impl FromStr for AudioFormat { - type Err = (); - fn from_str(s: &str) -> Result { - match s.to_uppercase().as_ref() { - "F64" => Ok(Self::F64), +impl TryFrom<&String> for AudioFormat { + type Error = (); + fn try_from(s: &String) -> Result { + match s.to_uppercase().as_str() { "F32" => Ok(Self::F32), "S32" => Ok(Self::S32), "S24" => Ok(Self::S24), @@ -67,8 +64,6 @@ impl AudioFormat { #[allow(dead_code)] pub fn size(&self) -> usize { match self { - Self::F64 => mem::size_of::(), - Self::F32 => mem::size_of::(), Self::S24_3 => mem::size_of::(), Self::S16 => mem::size_of::(), _ => mem::size_of::(), // S32 and S24 are both stored in i32 @@ -85,7 +80,7 @@ pub enum NormalisationType { impl FromStr for NormalisationType { type Err = (); fn from_str(s: &str) -> Result { - match s.to_lowercase().as_ref() { + match s { "album" => Ok(Self::Album), "track" => Ok(Self::Track), _ => Err(()), @@ -108,7 +103,7 @@ pub enum NormalisationMethod { impl FromStr for NormalisationMethod { type Err = (); fn from_str(s: &str) -> Result { - match s.to_lowercase().as_ref() { + match s { "basic" => Ok(Self::Basic), "dynamic" => Ok(Self::Dynamic), _ => Err(()), @@ -122,81 +117,35 @@ impl Default for NormalisationMethod { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct PlayerConfig { pub bitrate: Bitrate, - pub gapless: bool, - pub passthrough: bool, - pub normalisation: bool, pub normalisation_type: NormalisationType, pub normalisation_method: NormalisationMethod, - pub normalisation_pregain: f64, - pub normalisation_threshold: f64, - pub normalisation_attack: Duration, - pub normalisation_release: Duration, - pub normalisation_knee: f64, - - // pass function pointers so they can be lazily instantiated *after* spawning a thread - // (thereby circumventing Send bounds that they might not satisfy) - pub ditherer: Option, + pub normalisation_pregain: f32, + pub normalisation_threshold: f32, + pub normalisation_attack: f32, + pub normalisation_release: f32, + pub normalisation_knee: f32, + pub gapless: bool, + pub passthrough: bool, } impl Default for PlayerConfig { - fn default() -> Self { - Self { + fn default() -> PlayerConfig { + PlayerConfig { bitrate: Bitrate::default(), - gapless: true, normalisation: false, normalisation_type: NormalisationType::default(), normalisation_method: NormalisationMethod::default(), normalisation_pregain: 0.0, - normalisation_threshold: db_to_ratio(-1.0), - normalisation_attack: Duration::from_millis(5), - normalisation_release: Duration::from_millis(100), + normalisation_threshold: NormalisationData::db_to_ratio(-1.0), + normalisation_attack: 0.005, + normalisation_release: 0.1, normalisation_knee: 1.0, + gapless: true, passthrough: false, - ditherer: Some(mk_ditherer::), - } - } -} - -// fields are intended for volume control range in dB -#[derive(Clone, Copy, Debug)] -pub enum VolumeCtrl { - Cubic(f64), - Fixed, - Linear, - Log(f64), -} - -impl FromStr for VolumeCtrl { - type Err = (); - fn from_str(s: &str) -> Result { - Self::from_str_with_range(s, Self::DEFAULT_DB_RANGE) - } -} - -impl Default for VolumeCtrl { - fn default() -> VolumeCtrl { - VolumeCtrl::Log(Self::DEFAULT_DB_RANGE) - } -} - -impl VolumeCtrl { - pub const MAX_VOLUME: u16 = u16::MAX; - - // Taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html - pub const DEFAULT_DB_RANGE: f64 = 60.0; - - pub fn from_str_with_range(s: &str, db_range: f64) -> Result::Err> { - use self::VolumeCtrl::*; - match s.to_lowercase().as_ref() { - "cubic" => Ok(Cubic(db_range)), - "fixed" => Ok(Fixed), - "linear" => Ok(Linear), - "log" => Ok(Log(db_range)), - _ => Err(()), } } } diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 962ade66..450910b0 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -1,4 +1,3 @@ -use crate::dither::{Ditherer, DithererBuilder}; use zerocopy::AsBytes; #[derive(AsBytes, Copy, Clone, Debug)] @@ -6,122 +5,52 @@ use zerocopy::AsBytes; #[repr(transparent)] pub struct i24([u8; 3]); impl i24 { - fn from_s24(sample: i32) -> Self { - // trim the padding in the most significant byte - #[allow(unused_variables)] - let [a, b, c, d] = sample.to_ne_bytes(); - #[cfg(target_endian = "little")] - return Self([a, b, c]); - #[cfg(target_endian = "big")] - return Self([b, c, d]); + fn pcm_from_i32(sample: i32) -> Self { + // drop the least significant byte + let [a, b, c, _d] = (sample >> 8).to_le_bytes(); + i24([a, b, c]) } } -pub struct Converter { - ditherer: Option>, -} - -impl Converter { - pub fn new(dither_config: Option) -> Self { - if let Some(ref ditherer_builder) = dither_config { - let ditherer = (ditherer_builder)(); - info!("Converting with ditherer: {}", ditherer.name()); - Self { - ditherer: Some(ditherer), - } - } else { - Self { ditherer: None } - } - } - - /// To convert PCM samples from floating point normalized as `-1.0..=1.0` - /// to 32-bit signed integer, multiply by 2147483648 (0x80000000) and - /// saturate at the bounds of `i32`. - const SCALE_S32: f64 = 2147483648.; - - /// To convert PCM samples from floating point normalized as `-1.0..=1.0` - /// to 24-bit signed integer, multiply by 8388608 (0x800000) and saturate - /// at the bounds of `i24`. - const SCALE_S24: f64 = 8388608.; - - /// To convert PCM samples from floating point normalized as `-1.0..=1.0` - /// to 16-bit signed integer, multiply by 32768 (0x8000) and saturate at - /// the bounds of `i16`. When the samples were encoded using the same - /// scaling factor, like the reference Vorbis encoder does, this makes - /// conversions transparent. - const SCALE_S16: f64 = 32768.; - - pub fn scale(&mut self, sample: f64, factor: f64) -> f64 { - let dither = match self.ditherer { - Some(ref mut d) => d.noise(), - None => 0.0, - }; - - // From the many float to int conversion methods available, match what - // the reference Vorbis implementation uses: sample * 32768 (for 16 bit) - let int_value = sample * factor + dither; - - // Casting float to integer rounds towards zero by default, i.e. it - // truncates, and that generates larger error than rounding to nearest. - int_value.round() - } - - // Special case for samples packed in a word of greater bit depth (e.g. - // S24): clamp between min and max to ensure that the most significant - // byte is zero. Otherwise, dithering may cause an overflow. This is not - // necessary for other formats, because casting to integer will saturate - // to the bounds of the primitive. - pub fn clamping_scale(&mut self, sample: f64, factor: f64) -> f64 { - let int_value = self.scale(sample, factor); - - // In two's complement, there are more negative than positive values. - let min = -factor; - let max = factor - 1.0; - - if int_value < min { - return min; - } else if int_value > max { - return max; - } - int_value - } - - pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec { - samples.iter().map(|sample| *sample as f32).collect() - } - - pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec { - samples - .iter() - .map(|sample| self.scale(*sample, Self::SCALE_S32) as i32) - .collect() - } - - // S24 is 24-bit PCM packed in an upper 32-bit word - pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec { - samples - .iter() - .map(|sample| self.clamping_scale(*sample, Self::SCALE_S24) as i32) - .collect() - } - - // S24_3 is 24-bit PCM in a 3-byte array - pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec { - samples +// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. +macro_rules! convert_samples_to { + ($type: ident, $samples: expr) => { + convert_samples_to!($type, $samples, 0) + }; + ($type: ident, $samples: expr, $drop_bits: expr) => { + $samples .iter() .map(|sample| { - // Not as DRY as calling f32_to_s24 first, but this saves iterating - // over all samples twice. - let int_value = self.clamping_scale(*sample, Self::SCALE_S24) as i32; - i24::from_s24(int_value) + // Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] + // while maintaining DC linearity. There is nothing to be gained + // by doing this in f64, as the significand of a f32 is 24 bits, + // just like the maximum bit depth we are converting to. + let int_value = *sample * (std::$type::MAX as f32 + 0.5) - 0.5; + + // Casting floats to ints truncates by default, which results + // in larger quantization error than rounding arithmetically. + // Flooring is faster, but again with larger error. + int_value.round() as $type >> $drop_bits }) .collect() - } - - pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec { - samples - .iter() - .map(|sample| self.scale(*sample, Self::SCALE_S16) as i16) - .collect() - } + }; +} + +pub fn to_s32(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples) +} + +pub fn to_s24(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples, 8) +} + +pub fn to_s24_3(samples: &[f32]) -> Vec { + to_s32(samples) + .iter() + .map(|sample| i24::pcm_from_i32(*sample)) + .collect() +} + +pub fn to_s16(samples: &[f32]) -> Vec { + convert_samples_to!(i16, samples) } diff --git a/playback/src/decoder/lewton_decoder.rs b/playback/src/decoder/lewton_decoder.rs index adf63e2a..528d9344 100644 --- a/playback/src/decoder/lewton_decoder.rs +++ b/playback/src/decoder/lewton_decoder.rs @@ -1,12 +1,10 @@ use super::{AudioDecoder, AudioError, AudioPacket}; use lewton::inside_ogg::OggStreamReader; -use lewton::samples::InterleavedSamples; use std::error; use std::fmt; use std::io::{Read, Seek}; -use std::time::Duration; pub struct VorbisDecoder(OggStreamReader); pub struct VorbisError(lewton::VorbisError); @@ -25,7 +23,7 @@ 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(); + let absgp = ms * 44100 / 1000; match self.0.seek_absgp_pg(absgp as u64) { Ok(_) => Ok(()), Err(err) => Err(AudioError::VorbisError(err.into())), @@ -37,8 +35,11 @@ where use lewton::OggReadError::NoCapturePatternFound; use lewton::VorbisError::{BadAudio, OggError}; loop { - match self.0.read_dec_packet_generic::>() { - Ok(Some(packet)) => return Ok(Some(AudioPacket::samples_from_f32(packet.samples))), + match self + .0 + .read_dec_packet_generic::>() + { + Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))), Ok(None) => return Ok(None), Err(BadAudio(AudioIsHeader)) => (), diff --git a/playback/src/decoder/libvorbis_decoder.rs b/playback/src/decoder/libvorbis_decoder.rs new file mode 100644 index 00000000..6f9a68a3 --- /dev/null +++ b/playback/src/decoder/libvorbis_decoder.rs @@ -0,0 +1,89 @@ +#[cfg(feature = "with-tremor")] +use librespot_tremor as vorbis; + +use super::{AudioDecoder, AudioError, AudioPacket}; +use std::error; +use std::fmt; +use std::io::{Read, Seek}; + +pub struct VorbisDecoder(vorbis::Decoder); +pub struct VorbisError(vorbis::VorbisError); + +impl VorbisDecoder +where + R: Read + Seek, +{ + pub fn new(input: R) -> Result, VorbisError> { + Ok(VorbisDecoder(vorbis::Decoder::new(input)?)) + } +} + +impl AudioDecoder for VorbisDecoder +where + R: Read + Seek, +{ + #[cfg(not(feature = "with-tremor"))] + fn seek(&mut self, ms: i64) -> Result<(), AudioError> { + self.0.time_seek(ms as f64 / 1000f64)?; + Ok(()) + } + + #[cfg(feature = "with-tremor")] + fn seek(&mut self, ms: i64) -> Result<(), AudioError> { + self.0.time_seek(ms)?; + Ok(()) + } + + fn next_packet(&mut self) -> Result, AudioError> { + loop { + match self.0.packets().next() { + Some(Ok(packet)) => { + // Losslessly represent [-32768, 32767] to [-1.0, 1.0] while maintaining DC linearity. + return Ok(Some(AudioPacket::Samples( + packet + .data + .iter() + .map(|sample| { + ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32 + }) + .collect(), + ))); + } + None => return Ok(None), + + Some(Err(vorbis::VorbisError::Hole)) => (), + Some(Err(err)) => return Err(err.into()), + } + } + } +} + +impl From for VorbisError { + fn from(err: vorbis::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) + } +} + +impl From for AudioError { + fn from(err: vorbis::VorbisError) -> AudioError { + AudioError::VorbisError(VorbisError(err)) + } +} diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index 9641e8b3..6108f00f 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -1,23 +1,27 @@ use std::fmt; -mod lewton_decoder; -pub use lewton_decoder::{VorbisDecoder, VorbisError}; +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] { + mod libvorbis_decoder; + pub use libvorbis_decoder::{VorbisDecoder, VorbisError}; + } else { + mod lewton_decoder; + pub use lewton_decoder::{VorbisDecoder, VorbisError}; + } +} mod passthrough_decoder; pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; pub enum AudioPacket { - Samples(Vec), + Samples(Vec), OggData(Vec), } impl AudioPacket { - pub fn samples_from_f32(f32_samples: Vec) -> Self { - let f64_samples = f32_samples.iter().map(|sample| *sample as f64).collect(); - AudioPacket::Samples(f64_samples) - } - - pub fn samples(&self) -> &[f64] { + pub fn samples(&self) -> &[f32] { match self { AudioPacket::Samples(s) => s, AudioPacket::OggData(_) => panic!("can't return OggData on samples"), diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index 7c1ad532..e064cba3 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -1,10 +1,8 @@ // Passthrough decoder for librespot use super::{AudioDecoder, AudioError, AudioPacket}; -use crate::SAMPLE_RATE; 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> @@ -14,7 +12,7 @@ where let pck: Packet = rdr.read_packet_expected()?; let pkt_type = pck.data[0]; - debug!("Vorbis header type {}", &pkt_type); + debug!("Vorbis header type{}", &pkt_type); if pkt_type != code { return Err(PassthroughError(OggReadError::InvalidData)); @@ -98,10 +96,7 @@ impl AudioDecoder for PassthroughDecoder { 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, (ms * 44100 / 1000) as u64) { Ok(_) => { // need to set some offset for next_page() let pck = self.rdr.read_packet().unwrap().unwrap(); diff --git a/playback/src/dither.rs b/playback/src/dither.rs deleted file mode 100644 index 2510b886..00000000 --- a/playback/src/dither.rs +++ /dev/null @@ -1,150 +0,0 @@ -use rand::rngs::ThreadRng; -use rand_distr::{Distribution, Normal, Triangular, Uniform}; -use std::fmt; - -const NUM_CHANNELS: usize = 2; - -// Dithering lowers digital-to-analog conversion ("requantization") error, -// linearizing output, lowering distortion and replacing it with a constant, -// fixed noise level, which is more pleasant to the ear than the distortion. -// -// Guidance: -// -// * On S24, S24_3 and S24, the default is to use triangular dithering. -// Depending on personal preference you may use Gaussian dithering instead; -// it's not as good objectively, but it may be preferred subjectively if -// you are looking for a more "analog" sound akin to tape hiss. -// -// * Advanced users who know that they have a DAC without noise shaping have -// a third option: high-passed dithering, which is like triangular dithering -// except that it moves dithering noise up in frequency where it is less -// audible. Note: 99% of DACs are of delta-sigma design with noise shaping, -// so unless you have a multibit / R2R DAC, or otherwise know what you are -// doing, this is not for you. -// -// * Don't dither or shape noise on S32 or F32. On F32 it's not supported -// anyway (there are no integer conversions and so no rounding errors) and -// on S32 the noise level is so far down that it is simply inaudible even -// after volume normalisation and control. -// -pub trait Ditherer { - fn new() -> Self - where - Self: Sized; - fn name(&self) -> &'static str; - fn noise(&mut self) -> f64; -} - -impl fmt::Display for dyn Ditherer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.name()) - } -} - -// 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. -// - -pub struct TriangularDitherer { - cached_rng: ThreadRng, - distribution: Triangular, -} - -impl Ditherer for TriangularDitherer { - fn new() -> Self { - Self { - cached_rng: rand::thread_rng(), - // 2 LSB peak-to-peak needed to linearize the response: - distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(), - } - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn noise(&mut self) -> f64 { - self.distribution.sample(&mut self.cached_rng) - } -} - -impl TriangularDitherer { - pub const NAME: &'static str = "tpdf"; -} - -pub struct GaussianDitherer { - cached_rng: ThreadRng, - distribution: Normal, -} - -impl Ditherer for GaussianDitherer { - fn new() -> Self { - Self { - cached_rng: rand::thread_rng(), - // 1/2 LSB RMS needed to linearize the response: - distribution: Normal::new(0.0, 0.5).unwrap(), - } - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn noise(&mut self) -> f64 { - self.distribution.sample(&mut self.cached_rng) - } -} - -impl GaussianDitherer { - pub const NAME: &'static str = "gpdf"; -} - -pub struct HighPassDitherer { - active_channel: usize, - previous_noises: [f64; NUM_CHANNELS], - cached_rng: ThreadRng, - distribution: Uniform, -} - -impl Ditherer for HighPassDitherer { - fn new() -> Self { - Self { - active_channel: 0, - previous_noises: [0.0; NUM_CHANNELS], - cached_rng: rand::thread_rng(), - distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB - } - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn noise(&mut self) -> f64 { - let new_noise = self.distribution.sample(&mut self.cached_rng); - let high_passed_noise = new_noise - self.previous_noises[self.active_channel]; - self.previous_noises[self.active_channel] = new_noise; - self.active_channel ^= 1; - high_passed_noise - } -} - -impl HighPassDitherer { - pub const NAME: &'static str = "tpdf_hp"; -} - -pub fn mk_ditherer() -> Box { - Box::new(D::new()) -} - -pub type DithererBuilder = fn() -> Box; - -pub fn find_ditherer(name: Option) -> Option { - match name.as_deref() { - Some(TriangularDitherer::NAME) => Some(mk_ditherer::), - Some(GaussianDitherer::NAME) => Some(mk_ditherer::), - Some(HighPassDitherer::NAME) => Some(mk_ditherer::), - _ => None, - } -} diff --git a/playback/src/lib.rs b/playback/src/lib.rs index 689b8470..58423380 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -9,10 +9,5 @@ pub mod audio_backend; pub mod config; mod convert; mod decoder; -pub mod dither; pub mod mixer; 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; diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 8bee9e0d..5e0a963f 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -1,266 +1,218 @@ -use crate::player::{db_to_ratio, ratio_to_db}; +use super::AudioFilter; +use super::{Mixer, MixerConfig}; +use std::error::Error; -use super::mappings::{LogMapping, MappedCtrl, VolumeMapping}; -use super::{Mixer, MixerConfig, VolumeCtrl}; +const SND_CTL_TLV_DB_GAIN_MUTE: i64 = -9999999; -use alsa::ctl::{ElemId, ElemIface}; -use alsa::mixer::{MilliBel, SelemChannelId, SelemId}; -use alsa::{Ctl, Round}; - -use std::ffi::CString; +#[derive(Clone)] +struct AlsaMixerVolumeParams { + min: i64, + max: i64, + range: f64, + min_db: alsa::mixer::MilliBel, + max_db: alsa::mixer::MilliBel, + has_switch: bool, +} #[derive(Clone)] pub struct AlsaMixer { config: MixerConfig, - min: i64, - max: i64, - range: i64, - min_db: f64, - max_db: f64, - db_range: f64, - has_switch: bool, - is_softvol: bool, - use_linear_in_db: bool, -} - -// min_db cannot be depended on to be mute. Also note that contrary to -// its name copied verbatim from Alsa, this is in millibel scale. -const SND_CTL_TLV_DB_GAIN_MUTE: MilliBel = MilliBel(-9999999); -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, - ); - - let mut config = config; // clone - - let mixer = - alsa::mixer::Mixer::new(&config.card, 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"); - - // Query capabilities - let has_switch = simple_element.has_playback_switch(); - let is_softvol = simple_element - .get_playback_vol_db(SelemChannelId::mono()) - .is_err(); - - // Query raw volume range - let (min, max) = simple_element.get_playback_volume_range(); - let range = i64::abs(max - min); - - // 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 mut element_id = ElemId::new(ElemIface::Mixer); - element_id.set_name( - &CString::new(config.control.as_str()) - .expect("Could not open Alsa softvol with that name"), - ); - element_id.set_index(config.index); - let (min_millibel, mut max_millibel) = control - .get_db_range(&element_id) - .expect("Could not get Alsa softvol dB range"); - - // Alsa can report incorrect maximum volumes due to rounding - // errors. e.g. Alsa rounds [-60.0..0.0] in range [0..255] to - // step size 0.23. Then multiplying 0.23 by 255 incorrectly - // returns a dB range of 58.65 instead of 60 dB, from - // [-60.00..-1.35]. This workaround checks the default case - // where the maximum dB volume is expected to be 0, and cannot - // cover all cases. - if max_millibel != ZERO_DB { - warn!("Alsa mixer reported maximum dB != 0, which is suspect"); - let reported_step_size = (max_millibel - min_millibel).0 / range; - let assumed_step_size = (ZERO_DB - min_millibel).0 / range; - if reported_step_size == assumed_step_size { - warn!("Alsa rounding error detected, setting maximum dB to {:.2} instead of {:.2}", ZERO_DB.to_db(), max_millibel.to_db()); - max_millibel = ZERO_DB; - } else { - warn!("Please manually set with `--volume-ctrl` if this is incorrect"); - } - } - (min_millibel, max_millibel) - } else { - let (mut min_millibel, max_millibel) = simple_element.get_playback_db_range(); - - // Some controls report that their minimum volume is mute, instead - // of their actual lowest dB setting before that. - if min_millibel == SND_CTL_TLV_DB_GAIN_MUTE && min < max { - debug!("Alsa mixer reported minimum dB as mute, trying workaround"); - min_millibel = simple_element - .ask_playback_vol_db(min + 1) - .expect("Could not convert Alsa raw volume to dB volume"); - } - (min_millibel, max_millibel) - }; - - let min_db = min_millibel.to_db() as f64; - let max_db = max_millibel.to_db() as f64; - let db_range = f64::abs(max_db - min_db); - - // Synchronize the volume control dB range with the mixer control, - // unless it was already set with a command line option. - if !config.volume_ctrl.range_ok() { - config.volume_ctrl.set_db_range(db_range); - } - - // For hardware controls with a small range (24 dB or less), - // force using the dB API with a linear mapping. - let mut use_linear_in_db = false; - if !is_softvol && db_range <= 24.0 { - use_linear_in_db = true; - config.volume_ctrl = VolumeCtrl::Linear; - } - - debug!("Alsa mixer control is softvol: {}", is_softvol); - debug!("Alsa support for playback (mute) switch: {}", has_switch); - debug!("Alsa raw volume range: [{}..{}] ({})", min, max, range); - debug!( - "Alsa dB volume range: [{:.2}..{:.2}] ({:.2})", - min_db, max_db, db_range - ); - debug!("Alsa forcing linear dB mapping: {}", use_linear_in_db); - - Self { - config, - min, - max, - range, - min_db, - max_db, - db_range, - has_switch, - is_softvol, - use_linear_in_db, - } - } - - fn volume(&self) -> u16 { - let mixer = - alsa::mixer::Mixer::new(&self.config.card, 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"); - - if self.switched_off() { - return 0; - } - - let mut mapped_volume = if self.is_softvol { - let raw_volume = simple_element - .get_playback_volume(SelemChannelId::mono()) - .expect("Could not get raw Alsa volume"); - raw_volume as f64 / self.range as f64 - self.min as f64 - } else { - let db_volume = simple_element - .get_playback_vol_db(SelemChannelId::mono()) - .expect("Could not get Alsa dB volume") - .to_db() as f64; - - if self.use_linear_in_db { - (db_volume - self.min_db) / self.db_range - } else if f64::abs(db_volume - SND_CTL_TLV_DB_GAIN_MUTE.to_db() as f64) <= f64::EPSILON - { - 0.0 - } else { - db_to_ratio(db_volume - self.max_db) - } - }; - - // see comment in `set_volume` why we are handling an antilog volume - if mapped_volume > 0.0 && self.is_some_linear() { - mapped_volume = LogMapping::linear_to_mapped(mapped_volume, self.db_range); - } - - self.config.volume_ctrl.from_mapped(mapped_volume) - } - - fn set_volume(&self, volume: u16) { - let mixer = - alsa::mixer::Mixer::new(&self.config.card, 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"); - - if self.has_switch { - if volume == 0 { - debug!("Disabling playback (setting mute) on Alsa"); - simple_element - .set_playback_switch_all(0) - .expect("Could not disable playback (set mute) on Alsa"); - } else if self.switched_off() { - debug!("Enabling playback (unsetting mute) on Alsa"); - simple_element - .set_playback_switch_all(1) - .expect("Could not enable playback (unset mute) on Alsa"); - } - } - - let mut mapped_volume = self.config.volume_ctrl.to_mapped(volume); - - // Alsa's linear algorithms map everything onto log. Alsa softvol does - // this internally. In the case of `use_linear_in_db` this happens - // automatically by virtue of the dB scale. This means that linear - // controls become log, log becomes log-on-log, and so on. To make - // the controls work as expected, perform an antilog calculation to - // counteract what Alsa will be doing to the set volume. - if mapped_volume > 0.0 && self.is_some_linear() { - mapped_volume = LogMapping::mapped_to_linear(mapped_volume, self.db_range); - } - - if self.is_softvol { - let scaled_volume = (self.min as f64 + mapped_volume * self.range as f64) as i64; - debug!("Setting Alsa raw volume to {}", scaled_volume); - simple_element - .set_playback_volume_all(scaled_volume) - .expect("Could not set Alsa raw volume"); - return; - } - - let db_volume = if self.use_linear_in_db { - self.min_db + mapped_volume * self.db_range - } else if volume == 0 { - // prevent ratio_to_db(0.0) from returning -inf - SND_CTL_TLV_DB_GAIN_MUTE.to_db() as f64 - } else { - ratio_to_db(mapped_volume) + self.max_db - }; - - debug!("Setting Alsa volume to {:.2} dB", db_volume); - simple_element - .set_playback_db_all(MilliBel::from_db(db_volume as f32), Round::Floor) - .expect("Could not set Alsa dB volume"); - } + params: AlsaMixerVolumeParams, } impl AlsaMixer { - pub const NAME: &'static str = "alsa"; + fn pvol(&self, vol: T, min: T, max: T) -> f64 + where + T: std::ops::Sub + Copy, + f64: std::convert::From<::Output>, + { + f64::from(vol - min) / f64::from(max - min) + } - fn switched_off(&self) -> bool { - if !self.has_switch { - return false; + fn init_mixer(mut config: MixerConfig) -> Result> { + let mixer = alsa::mixer::Mixer::new(&config.card, false)?; + let sid = alsa::mixer::SelemId::new(&config.mixer, config.index); + + let selem = mixer.find_selem(&sid).unwrap_or_else(|| { + panic!( + "Couldn't find simple mixer control for {},{}", + &config.mixer, &config.index, + ) + }); + let (min, max) = selem.get_playback_volume_range(); + let (min_db, max_db) = selem.get_playback_db_range(); + let hw_mix = selem + .get_playback_vol_db(alsa::mixer::SelemChannelId::mono()) + .is_ok(); + let has_switch = selem.has_playback_switch(); + if min_db != alsa::mixer::MilliBel(SND_CTL_TLV_DB_GAIN_MUTE) { + warn!("Alsa min-db is not SND_CTL_TLV_DB_GAIN_MUTE!!"); + } + info!( + "Alsa Mixer info min: {} ({:?}[dB]) -- max: {} ({:?}[dB]) HW: {:?}", + min, min_db, max, max_db, hw_mix + ); + + if config.mapped_volume && (max_db - min_db <= alsa::mixer::MilliBel(24)) { + warn!( + "Switching to linear volume mapping, control range: {:?}", + max_db - min_db + ); + config.mapped_volume = false; + } else if !config.mapped_volume { + info!("Using Alsa linear volume"); } - let mixer = - alsa::mixer::Mixer::new(&self.config.card, 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"); + if min_db != alsa::mixer::MilliBel(SND_CTL_TLV_DB_GAIN_MUTE) { + debug!("Alsa min-db is not SND_CTL_TLV_DB_GAIN_MUTE!!"); + } - simple_element - .get_playback_switch(SelemChannelId::mono()) - .map(|playback| playback == 0) - .unwrap_or(false) + Ok(AlsaMixer { + config, + params: AlsaMixerVolumeParams { + min, + max, + range: (max - min) as f64, + min_db, + max_db, + has_switch, + }, + }) } - fn is_some_linear(&self) -> bool { - self.is_softvol || self.use_linear_in_db + fn map_volume(&self, set_volume: Option) -> Result> { + let mixer = alsa::mixer::Mixer::new(&self.config.card, false)?; + let sid = alsa::mixer::SelemId::new(&*self.config.mixer, self.config.index); + + let selem = mixer.find_selem(&sid).unwrap(); + let cur_vol = selem + .get_playback_volume(alsa::mixer::SelemChannelId::mono()) + .expect("Couldn't get current volume"); + let cur_vol_db = selem + .get_playback_vol_db(alsa::mixer::SelemChannelId::mono()) + .unwrap_or(alsa::mixer::MilliBel(-SND_CTL_TLV_DB_GAIN_MUTE)); + + let mut new_vol: u16 = 0; + trace!("Current alsa volume: {}{:?}", cur_vol, cur_vol_db); + + match set_volume { + Some(vol) => { + if self.params.has_switch { + let is_muted = selem + .get_playback_switch(alsa::mixer::SelemChannelId::mono()) + .map(|b| b == 0) + .unwrap_or(false); + if vol == 0 { + debug!("Toggling mute::True"); + selem.set_playback_switch_all(0).expect("Can't switch mute"); + + return Ok(vol); + } else if is_muted { + debug!("Toggling mute::False"); + selem.set_playback_switch_all(1).expect("Can't reset mute"); + } + } + + if self.config.mapped_volume { + // Cubic mapping ala alsamixer + // https://linux.die.net/man/1/alsamixer + // In alsamixer, the volume is mapped to a value that is more natural for a + // human ear. The mapping is designed so that the position in the interval is + // proportional to the volume as a human ear would perceive it, i.e. the + // position is the cubic root of the linear sample multiplication factor. For + // controls with a small range (24 dB or less), the mapping is linear in the dB + // values so that each step has the same size visually. TODO + // TODO: Check if min is not mute! + let vol_db = (self.pvol(vol, 0x0000, 0xFFFF).log10() * 6000.0).floor() as i64 + + self.params.max_db.0; + selem + .set_playback_db_all(alsa::mixer::MilliBel(vol_db), alsa::Round::Floor) + .expect("Couldn't set alsa dB volume"); + debug!( + "Mapping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [dB] - {} [i64]", + self.pvol(vol, 0x0000, 0xFFFF) * 100.0, + vol, + self.pvol( + vol_db as f64, + self.params.min as f64, + self.params.max as f64 + ) * 100.0, + vol_db as f64 / 100.0, + vol_db + ); + } else { + // Linear mapping + let alsa_volume = + ((vol as f64 / 0xFFFF as f64) * self.params.range) as i64 + self.params.min; + selem + .set_playback_volume_all(alsa_volume) + .expect("Couldn't set alsa raw volume"); + debug!( + "Mapping volume [{:.3}%] {:?} [u16] ->> Alsa [{:.3}%] {:?} [i64]", + self.pvol(vol, 0x0000, 0xFFFF) * 100.0, + vol, + self.pvol( + alsa_volume as f64, + self.params.min as f64, + self.params.max as f64 + ) * 100.0, + alsa_volume + ); + }; + } + None => { + new_vol = (((cur_vol - self.params.min) as f64 / self.params.range) * 0xFFFF as f64) + as u16; + debug!( + "Mapping volume [{:.3}%] {:?} [u16] <<- Alsa [{:.3}%] {:?} [i64]", + self.pvol(new_vol, 0x0000, 0xFFFF), + new_vol, + self.pvol( + cur_vol as f64, + self.params.min as f64, + self.params.max as f64 + ), + cur_vol + ); + } + } + + Ok(new_vol) + } +} + +impl Mixer for AlsaMixer { + fn open(config: Option) -> AlsaMixer { + let config = config.unwrap_or_default(); + info!( + "Setting up new mixer: card:{} mixer:{} index:{}", + config.card, config.mixer, config.index + ); + AlsaMixer::init_mixer(config).expect("Error setting up mixer!") + } + + fn start(&self) {} + + fn stop(&self) {} + + fn volume(&self) -> u16 { + match self.map_volume(None) { + Ok(vol) => vol, + Err(e) => { + error!("Error getting volume for <{}>, {:?}", self.config.card, e); + 0 + } + } + } + + fn set_volume(&self, volume: u16) { + match self.map_volume(Some(volume)) { + Ok(_) => (), + Err(e) => error!("Error setting volume for <{}>, {:?}", self.config.card, e), + } + } + + fn get_audio_filter(&self) -> Option> { + None } } diff --git a/playback/src/mixer/mappings.rs b/playback/src/mixer/mappings.rs deleted file mode 100644 index 04cef439..00000000 --- a/playback/src/mixer/mappings.rs +++ /dev/null @@ -1,163 +0,0 @@ -use super::VolumeCtrl; -use crate::player::db_to_ratio; - -pub trait MappedCtrl { - fn to_mapped(&self, volume: u16) -> f64; - fn from_mapped(&self, mapped_volume: f64) -> u16; - - fn db_range(&self) -> f64; - fn set_db_range(&mut self, new_db_range: f64); - fn range_ok(&self) -> bool; -} - -impl MappedCtrl for VolumeCtrl { - fn to_mapped(&self, volume: u16) -> f64 { - // More than just an optimization, this ensures that zero volume is - // really mute (both the log and cubic equations would otherwise not - // reach zero). - if volume == 0 { - return 0.0; - } else if volume == Self::MAX_VOLUME { - // And limit in case of rounding errors (as is the case for log). - return 1.0; - } - - let normalized_volume = volume as f64 / Self::MAX_VOLUME as f64; - let mapped_volume = if self.range_ok() { - match *self { - Self::Cubic(db_range) => { - CubicMapping::linear_to_mapped(normalized_volume, db_range) - } - Self::Log(db_range) => LogMapping::linear_to_mapped(normalized_volume, db_range), - _ => normalized_volume, - } - } else { - // Ensure not to return -inf or NaN due to division by zero. - error!( - "{:?} does not work with 0 dB range, using linear mapping instead", - self - ); - normalized_volume - }; - - debug!( - "Input volume {} mapped to: {:.2}%", - volume, - mapped_volume * 100.0 - ); - - mapped_volume - } - - fn from_mapped(&self, mapped_volume: f64) -> u16 { - // More than just an optimization, this ensures that zero mapped volume - // is unmapped to non-negative real numbers (otherwise the log and cubic - // equations would respectively return -inf and -1/9.) - if f64::abs(mapped_volume - 0.0) <= f64::EPSILON { - return 0; - } else if f64::abs(mapped_volume - 1.0) <= f64::EPSILON { - return Self::MAX_VOLUME; - } - - let unmapped_volume = if self.range_ok() { - match *self { - Self::Cubic(db_range) => CubicMapping::mapped_to_linear(mapped_volume, db_range), - Self::Log(db_range) => LogMapping::mapped_to_linear(mapped_volume, db_range), - _ => mapped_volume, - } - } else { - // Ensure not to return -inf or NaN due to division by zero. - error!( - "{:?} does not work with 0 dB range, using linear mapping instead", - self - ); - mapped_volume - }; - - (unmapped_volume * Self::MAX_VOLUME as f64) as u16 - } - - fn db_range(&self) -> f64 { - match *self { - Self::Fixed => 0.0, - Self::Linear => Self::DEFAULT_DB_RANGE, // arbitrary, could be anything > 0 - Self::Log(db_range) | Self::Cubic(db_range) => db_range, - } - } - - fn set_db_range(&mut self, new_db_range: f64) { - match self { - Self::Cubic(ref mut db_range) | Self::Log(ref mut db_range) => *db_range = new_db_range, - _ => error!("Invalid to set dB range for volume control type {:?}", self), - } - - debug!("Volume control is now {:?}", self) - } - - fn range_ok(&self) -> bool { - self.db_range() > 0.0 || matches!(self, Self::Fixed | Self::Linear) - } -} - -pub trait VolumeMapping { - fn linear_to_mapped(unmapped_volume: f64, db_range: f64) -> f64; - fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64; -} - -// Volume conversion taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html#ideal2 -// -// As the human auditory system has a logarithmic sensitivity curve, this -// mapping results in a near linear loudness experience with the listener. -pub struct LogMapping {} -impl VolumeMapping for LogMapping { - fn linear_to_mapped(normalized_volume: f64, db_range: f64) -> f64 { - let (db_ratio, ideal_factor) = Self::coefficients(db_range); - f64::exp(ideal_factor * normalized_volume) / db_ratio - } - - fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64 { - let (db_ratio, ideal_factor) = Self::coefficients(db_range); - f64::ln(db_ratio * mapped_volume) / ideal_factor - } -} - -impl LogMapping { - fn coefficients(db_range: f64) -> (f64, f64) { - let db_ratio = db_to_ratio(db_range); - let ideal_factor = f64::ln(db_ratio); - (db_ratio, ideal_factor) - } -} - -// Ported from: https://github.com/alsa-project/alsa-utils/blob/master/alsamixer/volume_mapping.c -// which in turn was inspired by: https://www.robotplanet.dk/audio/audio_gui_design/ -// -// Though this mapping is computationally less expensive than the logarithmic -// mapping, it really does not matter as librespot memoizes the mapped value. -// Use this mapping if you have some reason to mimic Alsa's native mixer or -// prefer a more granular control in the upper volume range. -// -// Note: https://www.dr-lex.be/info-stuff/volumecontrols.html#ideal3 shows -// better approximations to the logarithmic curve but because we only intend -// to mimic Alsa here, we do not implement them. If your desire is to use a -// logarithmic mapping, then use that volume control. -pub struct CubicMapping {} -impl VolumeMapping for CubicMapping { - fn linear_to_mapped(normalized_volume: f64, db_range: f64) -> f64 { - let min_norm = Self::min_norm(db_range); - f64::powi(normalized_volume * (1.0 - min_norm) + min_norm, 3) - } - - fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64 { - let min_norm = Self::min_norm(db_range); - (mapped_volume.powf(1.0 / 3.0) - min_norm) / (1.0 - min_norm) - } -} - -impl CubicMapping { - fn min_norm(db_range: f64) -> f64 { - // Note that this 60.0 is unrelated to DEFAULT_DB_RANGE. - // Instead, it's the cubic voltage to dB ratio. - f64::powf(10.0, -1.0 * db_range / 60.0) - } -} diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index ed39582e..af41c6f4 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -1,28 +1,20 @@ -use crate::config::VolumeCtrl; - -pub mod mappings; -use self::mappings::MappedCtrl; - pub trait Mixer: Send { - fn open(config: MixerConfig) -> Self + fn open(_: Option) -> Self where Self: Sized; - + fn start(&self); + fn stop(&self); fn set_volume(&self, volume: u16); fn volume(&self) -> u16; - fn get_audio_filter(&self) -> Option> { None } } pub trait AudioFilter { - fn modify_stream(&self, data: &mut [f64]); + fn modify_stream(&self, data: &mut [f32]); } -pub mod softmixer; -use self::softmixer::SoftMixer; - #[cfg(feature = "alsa-backend")] pub mod alsamixer; #[cfg(feature = "alsa-backend")] @@ -31,33 +23,36 @@ use self::alsamixer::AlsaMixer; #[derive(Debug, Clone)] pub struct MixerConfig { pub card: String, - pub control: String, + pub mixer: String, pub index: u32, - pub volume_ctrl: VolumeCtrl, + pub mapped_volume: bool, } impl Default for MixerConfig { fn default() -> MixerConfig { MixerConfig { card: String::from("default"), - control: String::from("PCM"), + mixer: String::from("PCM"), index: 0, - volume_ctrl: VolumeCtrl::default(), + mapped_volume: true, } } } -pub type MixerFn = fn(MixerConfig) -> Box; +pub mod softmixer; +use self::softmixer::SoftMixer; -fn mk_sink(config: MixerConfig) -> Box { - Box::new(M::open(config)) +type MixerFn = fn(Option) -> Box; + +fn mk_sink(device: Option) -> Box { + Box::new(M::open(device)) } -pub fn find(name: Option<&str>) -> Option { - match name { - None | Some(SoftMixer::NAME) => Some(mk_sink::), +pub fn find>(name: Option) -> Option { + match name.as_ref().map(AsRef::as_ref) { + None | Some("softvol") => Some(mk_sink::), #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => Some(mk_sink::), + Some("alsa") => Some(mk_sink::), _ => None, } } diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 27448237..ec8ed6b2 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -1,40 +1,28 @@ -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use super::AudioFilter; -use super::{MappedCtrl, VolumeCtrl}; use super::{Mixer, MixerConfig}; #[derive(Clone)] pub struct SoftMixer { - // There is no AtomicF64, so we store the f64 as bits in a u64 field. - // It's much faster than a Mutex. - volume: Arc, - volume_ctrl: VolumeCtrl, + volume: Arc, } impl Mixer for SoftMixer { - fn open(config: MixerConfig) -> Self { - let volume_ctrl = config.volume_ctrl; - info!("Mixing with softvol and volume control: {:?}", volume_ctrl); - - Self { - volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))), - volume_ctrl, + fn open(_: Option) -> SoftMixer { + SoftMixer { + volume: Arc::new(AtomicUsize::new(0xFFFF)), } } - + fn start(&self) {} + fn stop(&self) {} fn volume(&self) -> u16 { - let mapped_volume = f64::from_bits(self.volume.load(Ordering::Relaxed)); - self.volume_ctrl.from_mapped(mapped_volume) + self.volume.load(Ordering::Relaxed) as u16 } - fn set_volume(&self, volume: u16) { - let mapped_volume = self.volume_ctrl.to_mapped(volume); - self.volume - .store(mapped_volume.to_bits(), Ordering::Relaxed) + self.volume.store(volume as usize, Ordering::Relaxed); } - fn get_audio_filter(&self) -> Option> { Some(Box::new(SoftVolumeApplier { volume: self.volume.clone(), @@ -42,20 +30,17 @@ impl Mixer for SoftMixer { } } -impl SoftMixer { - pub const NAME: &'static str = "softmixer"; -} - struct SoftVolumeApplier { - volume: Arc, + volume: Arc, } impl AudioFilter for SoftVolumeApplier { - fn modify_stream(&self, data: &mut [f64]) { - let volume = f64::from_bits(self.volume.load(Ordering::Relaxed)); - if volume < 1.0 { + fn modify_stream(&self, data: &mut [f32]) { + let volume = self.volume.load(Ordering::Relaxed) as u16; + if volume != 0xFFFF { + let volume_factor = volume as f64 / 0xFFFF as f64; for x in data.iter_mut() { - *x *= volume; + *x = (*x as f64 * volume_factor) as f32; } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 0249db9c..d67d2f88 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -2,7 +2,6 @@ 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}; @@ -14,12 +13,11 @@ use tokio::sync::{mpsc, oneshot}; use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController}; use crate::audio::{ - READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, - READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, + READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, + READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; 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; @@ -27,10 +25,12 @@ use crate::decoder::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; -use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND}; +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; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; -pub const DB_VOLTAGE_RATIO: f64 = 20.0; +const DB_VOLTAGE_RATIO: f32 = 20.0; pub struct Player { commands: Option>, @@ -59,14 +59,13 @@ struct PlayerInternal { sink_event_callback: Option, audio_filter: Option>, event_senders: Vec>, - converter: Converter, limiter_active: bool, limiter_attack_counter: u32, limiter_release_counter: u32, - limiter_peak_sample: f64, - limiter_factor: f64, - limiter_strength: f64, + limiter_peak_sample: f32, + limiter_factor: f32, + limiter_strength: f32, } enum PlayerCommand { @@ -197,14 +196,6 @@ impl PlayerEvent { pub type PlayerEventChannel = mpsc::UnboundedReceiver; -pub fn db_to_ratio(db: f64) -> f64 { - f64::powf(10.0, db / DB_VOLTAGE_RATIO) -} - -pub fn ratio_to_db(ratio: f64) -> f64 { - ratio.log10() * DB_VOLTAGE_RATIO -} - #[derive(Clone, Copy, Debug)] pub struct NormalisationData { track_gain_db: f32, @@ -214,6 +205,14 @@ pub struct NormalisationData { } impl NormalisationData { + pub fn db_to_ratio(db: f32) -> f32 { + f32::powf(10.0, db / DB_VOLTAGE_RATIO) + } + + pub fn ratio_to_db(ratio: f32) -> f32 { + ratio.log10() * DB_VOLTAGE_RATIO + } + 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))?; @@ -233,7 +232,7 @@ impl NormalisationData { Ok(r) } - fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f64 { + fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 { if !config.normalisation { return 1.0; } @@ -243,12 +242,12 @@ impl NormalisationData { NormalisationType::Track => [data.track_gain_db, data.track_peak], }; - let normalisation_power = gain_db as f64 + config.normalisation_pregain; - let mut normalisation_factor = db_to_ratio(normalisation_power); + let normalisation_power = gain_db + config.normalisation_pregain; + let mut normalisation_factor = Self::db_to_ratio(normalisation_power); - if normalisation_factor * gain_peak as f64 > config.normalisation_threshold { - let limited_normalisation_factor = config.normalisation_threshold / gain_peak as f64; - let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); + if normalisation_factor * gain_peak > config.normalisation_threshold { + let limited_normalisation_factor = config.normalisation_threshold / gain_peak; + let limited_normalisation_power = Self::ratio_to_db(limited_normalisation_factor); if config.normalisation_method == NormalisationMethod::Basic { warn!("Limiting gain to {:.2} dB for the duration of this track to stay under normalisation threshold.", limited_normalisation_power); @@ -264,9 +263,21 @@ impl NormalisationData { } debug!("Normalisation Data: {:?}", data); - debug!("Normalisation Factor: {:.2}%", normalisation_factor * 100.0); + debug!("Normalisation Type: {:?}", config.normalisation_type); + debug!( + "Normalisation Threshold: {:.1}", + Self::ratio_to_db(config.normalisation_threshold) + ); + debug!("Normalisation Method: {:?}", config.normalisation_method); + debug!("Normalisation Factor: {}", normalisation_factor); - normalisation_factor as f64 + if config.normalisation_method == NormalisationMethod::Dynamic { + debug!("Normalisation Attack: {:?}", config.normalisation_attack); + debug!("Normalisation Release: {:?}", config.normalisation_release); + debug!("Normalisation Knee: {:?}", config.normalisation_knee); + } + + normalisation_factor } } @@ -283,30 +294,9 @@ impl Player { let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); let (event_sender, event_receiver) = mpsc::unbounded_channel(); - if config.normalisation { - debug!("Normalisation Type: {:?}", config.normalisation_type); - debug!( - "Normalisation Pregain: {:.1} dB", - config.normalisation_pregain - ); - debug!( - "Normalisation Threshold: {:.1} dBFS", - ratio_to_db(config.normalisation_threshold) - ); - debug!("Normalisation Method: {:?}", config.normalisation_method); - - if config.normalisation_method == NormalisationMethod::Dynamic { - debug!("Normalisation Attack: {:?}", config.normalisation_attack); - debug!("Normalisation Release: {:?}", config.normalisation_release); - debug!("Normalisation Knee: {:?}", config.normalisation_knee); - } - } - let handle = thread::spawn(move || { debug!("new Player[{}]", session.session_id()); - let converter = Converter::new(config.ditherer); - let internal = PlayerInternal { session, config, @@ -319,7 +309,6 @@ impl Player { sink_event_callback: None, audio_filter, event_senders: [event_sender].to_vec(), - converter, limiter_active: false, limiter_attack_counter: 0, @@ -423,7 +412,7 @@ impl Drop for Player { struct PlayerLoadedTrackData { decoder: Decoder, - normalisation_factor: f64, + normalisation_factor: f32, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -456,7 +445,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, - normalisation_factor: f64, + normalisation_factor: f32, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -467,7 +456,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, - normalisation_factor: f64, + normalisation_factor: f32, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -779,7 +768,7 @@ impl PlayerTrackLoader { } Err(_) => { warn!("Unable to extract normalisation data, using default value."); - 1.0 + 1.0_f32 } }; @@ -963,12 +952,12 @@ impl Future for PlayerInternal { 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. + // 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 + lag > 1000 } }; if notify_about_position { @@ -1055,10 +1044,7 @@ impl PlayerInternal { } match self.sink.start() { Ok(()) => self.sink_status = SinkStatus::Running, - Err(err) => { - error!("Fatal error, could not start audio sink: {}", err); - exit(1); - } + Err(err) => error!("Could not start audio: {}", err), } } } @@ -1067,21 +1053,14 @@ impl PlayerInternal { match self.sink_status { SinkStatus::Running => { trace!("== Stopping sink =="); - match self.sink.stop() { - Ok(()) => { - self.sink_status = if temporarily { - SinkStatus::TemporarilyClosed - } else { - SinkStatus::Closed - }; - if let Some(callback) = &mut self.sink_event_callback { - callback(self.sink_status); - } - } - Err(err) => { - error!("Fatal error, could not stop audio sink: {}", err); - exit(1); - } + self.sink.stop().unwrap(); + self.sink_status = if temporarily { + SinkStatus::TemporarilyClosed + } else { + SinkStatus::Closed + }; + if let Some(callback) = &mut self.sink_event_callback { + callback(self.sink_status); } } SinkStatus::TemporarilyClosed => { @@ -1178,7 +1157,7 @@ impl PlayerInternal { } } - fn handle_packet(&mut self, packet: Option, normalisation_factor: f64) { + fn handle_packet(&mut self, packet: Option, normalisation_factor: f32) { match packet { Some(mut packet) => { if !packet.is_empty() { @@ -1188,7 +1167,7 @@ impl PlayerInternal { } if self.config.normalisation - && !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON + && !(f32::abs(normalisation_factor - 1.0) <= f32::EPSILON && self.config.normalisation_method == NormalisationMethod::Basic) { for sample in data.iter_mut() { @@ -1208,10 +1187,10 @@ impl PlayerInternal { { shaped_limiter_strength = 1.0 / (1.0 - + f64::powf( + + f32::powf( shaped_limiter_strength / (1.0 - shaped_limiter_strength), - -self.config.normalisation_knee, + -1.0 * self.config.normalisation_knee, )); } actual_normalisation_factor = @@ -1219,38 +1198,32 @@ impl PlayerInternal { + shaped_limiter_strength * self.limiter_factor; }; - // Cast the fields here for better readability - let normalisation_attack = - self.config.normalisation_attack.as_secs_f64(); - let normalisation_release = - self.config.normalisation_release.as_secs_f64(); - let limiter_release_counter = - self.limiter_release_counter as f64; - let limiter_attack_counter = self.limiter_attack_counter as f64; - let samples_per_second = SAMPLES_PER_SECOND as f64; - // Always check for peaks, even when the limiter is already active. // There may be even higher peaks than we initially targeted. // Check against the normalisation factor that would be applied normally. - let abs_sample = f64::abs(*sample * normalisation_factor); + let abs_sample = + ((*sample as f64 * normalisation_factor as f64) as f32) + .abs(); if abs_sample > self.config.normalisation_threshold { self.limiter_active = true; if self.limiter_release_counter > 0 { // A peak was encountered while releasing the limiter; // synchronize with the current release limiter strength. - self.limiter_attack_counter = (((samples_per_second - * normalisation_release) - - limiter_release_counter) - / (normalisation_release / normalisation_attack)) + self.limiter_attack_counter = (((SAMPLES_PER_SECOND + as f32 + * self.config.normalisation_release) + - self.limiter_release_counter as f32) + / (self.config.normalisation_release + / self.config.normalisation_attack)) as u32; self.limiter_release_counter = 0; } self.limiter_attack_counter = self.limiter_attack_counter.saturating_add(1); - - self.limiter_strength = limiter_attack_counter - / (samples_per_second * normalisation_attack); + self.limiter_strength = self.limiter_attack_counter as f32 + / (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_attack); if abs_sample > self.limiter_peak_sample { self.limiter_peak_sample = abs_sample; @@ -1264,10 +1237,12 @@ impl PlayerInternal { // the limiter reached full strength. For that reason // start the release by synchronizing with the current // attack limiter strength. - self.limiter_release_counter = (((samples_per_second - * normalisation_attack) - - limiter_attack_counter) - * (normalisation_release / normalisation_attack)) + self.limiter_release_counter = (((SAMPLES_PER_SECOND + as f32 + * self.config.normalisation_attack) + - self.limiter_attack_counter as f32) + * (self.config.normalisation_release + / self.config.normalisation_attack)) as u32; self.limiter_attack_counter = 0; } @@ -1276,19 +1251,23 @@ impl PlayerInternal { self.limiter_release_counter.saturating_add(1); if self.limiter_release_counter - > (samples_per_second * normalisation_release) as u32 + > (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release) + as u32 { self.reset_limiter(); } else { - self.limiter_strength = ((samples_per_second - * normalisation_release) - - limiter_release_counter) - / (samples_per_second * normalisation_release); + self.limiter_strength = ((SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release) + - self.limiter_release_counter as f32) + / (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release); } } } - *sample *= actual_normalisation_factor; + *sample = + (*sample as f64 * actual_normalisation_factor as f64) as f32; // Extremely sharp attacks, however unlikely, *may* still clip and provide // undefined results, so strictly enforce output within [-1.0, 1.0]. @@ -1301,9 +1280,9 @@ impl PlayerInternal { } } - if let Err(err) = self.sink.write(&packet, &mut self.converter) { - error!("Fatal error, could not write audio to audio sink: {}", err); - exit(1); + if let Err(err) = self.sink.write(&packet) { + error!("Could not write audio: {}", err); + self.ensure_sink_stopped(false); } } } @@ -1809,18 +1788,18 @@ impl PlayerInternal { // Request our read ahead range let request_data_length = max( (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS - * stream_loader_controller.ping_time().as_secs_f32() - * bytes_per_second as f32) as usize, - (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, + * (0.001 * stream_loader_controller.ping_time_ms() as f64) + * bytes_per_second as f64) as usize, + (READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, ); stream_loader_controller.fetch_next(request_data_length); // Request the part we want to wait for blocking. This effecively means we wait for the previous request to partially complete. let wait_for_data_length = max( (READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS - * stream_loader_controller.ping_time().as_secs_f32() - * bytes_per_second as f32) as usize, - (READ_AHEAD_BEFORE_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, + * (0.001 * stream_loader_controller.ping_time_ms() as f64) + * bytes_per_second as f64) as usize, + (READ_AHEAD_BEFORE_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, ); stream_loader_controller.fetch_next_blocking(wait_for_data_length); } diff --git a/src/lib.rs b/src/lib.rs index 75211282..7722e93e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ pub use librespot_audio as audio; pub use librespot_connect as connect; pub use librespot_core as core; -pub use librespot_discovery as discovery; pub use librespot_metadata as metadata; pub use librespot_playback as playback; pub use librespot_protocol as protocol; diff --git a/src/main.rs b/src/main.rs index a3687aaa..a5106af2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,31 +9,30 @@ 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::config::{ConnectConfig, DeviceType, SessionConfig, VolumeCtrl}; use librespot::core::session::Session; use librespot::core::version; -use librespot::playback::audio_backend::{self, SinkBuilder, BACKENDS}; +use librespot::playback::audio_backend::{self, Sink, BACKENDS}; use librespot::playback::config::{ - AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, + AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, }; -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::mixer::{self, Mixer, MixerConfig}; +use librespot::playback::player::{NormalisationData, 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::convert::TryFrom; use std::path::Path; -use std::pin::Pin; use std::process::exit; use std::str::FromStr; -use std::time::Duration; -use std::time::Instant; +use std::{env, time::Instant}; +use std::{ + io::{stderr, Write}, + pin::Pin, +}; + +const MILLIS: f32 = 1000.0; fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) @@ -67,7 +66,7 @@ fn setup_logging(verbose: bool) { } fn list_backends() { - println!("Available backends : "); + println!("Available Backends : "); for (&(name, _), idx) in BACKENDS.iter().zip(0..) { if idx == 0 { println!("- {} (default)", name); @@ -170,11 +169,14 @@ fn print_version() { ); } +#[derive(Clone)] struct Setup { format: AudioFormat, - backend: SinkBuilder, + backend: fn(Option, AudioFormat) -> Box, device: Option, - mixer: MixerFn, + + mixer: fn(Option) -> Box, + cache: Option, player_config: PlayerConfig, session_config: SessionConfig, @@ -188,242 +190,182 @@ struct Setup { } fn get_setup(args: &[String]) -> Setup { - const AP_PORT: &str = "ap-port"; - const AUTOPLAY: &str = "autoplay"; - const BACKEND: &str = "backend"; - const BITRATE: &str = "b"; - const CACHE: &str = "c"; - const CACHE_SIZE_LIMIT: &str = "cache-size-limit"; - const DEVICE: &str = "device"; - const DEVICE_TYPE: &str = "device-type"; - const DISABLE_AUDIO_CACHE: &str = "disable-audio-cache"; - const DISABLE_DISCOVERY: &str = "disable-discovery"; - const DISABLE_GAPLESS: &str = "disable-gapless"; - const DITHER: &str = "dither"; - 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 INITIAL_VOLUME: &str = "initial-volume"; - const MIXER_CARD: &str = "mixer-card"; - const MIXER_INDEX: &str = "mixer-index"; - const MIXER_NAME: &str = "mixer-name"; - const NAME: &str = "name"; - const NORMALISATION_ATTACK: &str = "normalisation-attack"; - const NORMALISATION_GAIN_TYPE: &str = "normalisation-gain-type"; - const NORMALISATION_KNEE: &str = "normalisation-knee"; - const NORMALISATION_METHOD: &str = "normalisation-method"; - const NORMALISATION_PREGAIN: &str = "normalisation-pregain"; - const NORMALISATION_RELEASE: &str = "normalisation-release"; - const NORMALISATION_THRESHOLD: &str = "normalisation-threshold"; - const ONEVENT: &str = "onevent"; - const PASSTHROUGH: &str = "passthrough"; - const PASSWORD: &str = "password"; - const PROXY: &str = "proxy"; - const SYSTEM_CACHE: &str = "system-cache"; - const USERNAME: &str = "username"; - const VERBOSE: &str = "verbose"; - const VERSION: &str = "version"; - const VOLUME_CTRL: &str = "volume-ctrl"; - const VOLUME_RANGE: &str = "volume-range"; - const ZEROCONF_PORT: &str = "zeroconf-port"; - let mut opts = getopts::Options::new(); - opts.optflag( - HELP, - "help", - "Print this help menu.", - ).optopt( - CACHE, + opts.optopt( + "c", "cache", "Path to a directory where files will be cached.", - "PATH", + "CACHE", ).optopt( "", - SYSTEM_CACHE, - "Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value.", - "PATH", + "system-cache", + "Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value", + "SYTEMCACHE", ).optopt( "", - CACHE_SIZE_LIMIT, + "cache-size-limit", "Limits the size of the cache for audio files.", - "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( - 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 to sign in with.", "USERNAME") - .optopt("p", PASSWORD, "Password", "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( - "", - 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.", - "NAME", - ) - .optopt( - "", - FORMAT, - "Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.", - "FORMAT", - ) - .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.", - "DITHER", - ) - .optopt("", "mixer", "Mixer to use {alsa|softvol}.", "MIXER") - .optopt( - "m", - MIXER_NAME, - "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", - ) - .optopt( - "", - MIXER_INDEX, - "Alsa index of the cards mixer. Defaults to 0.", - "INDEX", - ) - .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 advertised over zeroconf uses.", - "PORT", - ) - .optflag( - "", - ENABLE_VOLUME_NORMALISATION, - "Play all tracks at the same 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}. Defaults to album.", - "TYPE", - ) - .optopt( - "", - NORMALISATION_PREGAIN, - "Pregain (dB) applied by volume normalisation. Defaults to 0.", - "PREGAIN", - ) - .optopt( - "", - NORMALISATION_THRESHOLD, - "Threshold (dBFS) to prevent clipping. Defaults to -1.0.", - "THRESHOLD", - ) - .optopt( - "", - NORMALISATION_ATTACK, - "Attack time (ms) in which the dynamic limiter is reducing gain. Defaults to 5.", - "TIME", - ) - .optopt( - "", - NORMALISATION_RELEASE, - "Release or decay time (ms) in which the dynamic limiter is restoring gain. Defaults to 100.", - "TIME", - ) - .optopt( - "", - NORMALISATION_KNEE, - "Knee steepness of the dynamic limiter. Defaults to 1.0.", - "KNEE", - ) - .optopt( - "", - VOLUME_CTRL, - "Volume control type {cubic|fixed|linear|log}. Defaults to log.", - "VOLUME_CTRL" - ) - .optopt( - "", - VOLUME_RANGE, - "Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports.", - "RANGE", - ) - .optflag( - "", - AUTOPLAY, - "Automatically play similar songs when your music ends.", - ) - .optflag( - "", - DISABLE_GAPLESS, - "Disable gapless playback.", - ) - .optflag( - "", - PASSTHROUGH, - "Pass raw stream to output, only works for pipe and subprocess.", - ); + "CACHE_SIZE_LIMIT" + ).optflag("", "disable-audio-cache", "Disable caching of the audio data.") + .optopt("n", "name", "Device name", "NAME") + .optopt("", "device-type", "Displayed device type", "DEVICE_TYPE") + .optopt( + "b", + "bitrate", + "Bitrate (96, 160 or 320). Defaults to 160", + "BITRATE", + ) + .optopt( + "", + "onevent", + "Run PROGRAM when playback is about to begin.", + "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 to sign in with", "USERNAME") + .optopt("p", "password", "Password", "PASSWORD") + .optopt("", "proxy", "HTTP proxy to use when connecting", "PROXY") + .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", "AP_PORT") + .optflag("", "disable-discovery", "Disable discovery mode") + .optopt( + "", + "backend", + "Audio backend to use. Use '?' to list options", + "BACKEND", + ) + .optopt( + "", + "device", + "Audio device to use. Use '?' to list options if using portaudio or alsa", + "DEVICE", + ) + .optopt( + "", + "format", + "Output format (F32, S32, S24, S24_3 or S16). Defaults to S16", + "FORMAT", + ) + .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") + .optopt( + "m", + "mixer-name", + "Alsa mixer name, e.g \"PCM\" or \"Master\". Defaults to 'PCM'", + "MIXER_NAME", + ) + .optopt( + "", + "mixer-card", + "Alsa mixer card, e.g \"hw:0\" or similar from `aplay -l`. Defaults to 'default' ", + "MIXER_CARD", + ) + .optopt( + "", + "mixer-index", + "Alsa mixer index, Index of the cards mixer. Defaults to 0", + "MIXER_INDEX", + ) + .optflag( + "", + "mixer-linear-volume", + "Disable alsa's mapped volume scale (cubic). Default false", + ) + .optopt( + "", + "initial-volume", + "Initial volume in %, once connected (must be from 0 to 100)", + "VOLUME", + ) + .optopt( + "", + "zeroconf-port", + "The port the internal server advertised over zeroconf uses.", + "ZEROCONF_PORT", + ) + .optflag( + "", + "enable-volume-normalisation", + "Play all tracks at the same volume", + ) + .optopt( + "", + "normalisation-method", + "Specify the normalisation method to use - [basic, dynamic]. Default is dynamic.", + "NORMALISATION_METHOD", + ) + .optopt( + "", + "normalisation-gain-type", + "Specify the normalisation gain type to use - [track, album]. Default is album.", + "GAIN_TYPE", + ) + .optopt( + "", + "normalisation-pregain", + "Pregain (dB) applied by volume normalisation", + "PREGAIN", + ) + .optopt( + "", + "normalisation-threshold", + "Threshold (dBFS) to prevent clipping. Default is -1.0.", + "THRESHOLD", + ) + .optopt( + "", + "normalisation-attack", + "Attack time (ms) in which the dynamic limiter is reducing gain. Default is 5.", + "ATTACK", + ) + .optopt( + "", + "normalisation-release", + "Release or decay time (ms) in which the dynamic limiter is restoring gain. Default is 100.", + "RELEASE", + ) + .optopt( + "", + "normalisation-knee", + "Knee steepness of the dynamic limiter. Default is 1.0.", + "KNEE", + ) + .optopt( + "", + "volume-ctrl", + "Volume control type - [linear, log, fixed]. Default is logarithmic", + "VOLUME_CTRL" + ) + .optflag( + "", + "autoplay", + "autoplay similar songs when your music ends.", + ) + .optflag( + "", + "disable-gapless", + "disable gapless playback.", + ) + .optflag( + "", + "passthrough", + "Pass raw stream to output, only works for \"pipe\"." + ); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => { - eprintln!( - "Error parsing command line options: {}\n{}", - f, - usage(&args[0], &opts) - ); + eprintln!("error: {}\n{}", f.to_string(), usage(&args[0], &opts)); exit(1); } }; - if matches.opt_present(HELP) { - println!("{}", usage(&args[0], &opts)); - exit(0); - } - - if matches.opt_present(VERSION) { + if matches.opt_present("version") { print_version(); exit(0); } - let verbose = matches.opt_present(VERBOSE); + let verbose = matches.opt_present("verbose"); setup_logging(verbose); info!( @@ -434,7 +376,7 @@ fn get_setup(args: &[String]) -> Setup { build_id = version::BUILD_ID ); - let backend_name = matches.opt_str(BACKEND); + let backend_name = matches.opt_str("backend"); if backend_name == Some("?".into()) { list_backends(); exit(0); @@ -443,95 +385,57 @@ fn get_setup(args: &[String]) -> Setup { let backend = audio_backend::find(backend_name).expect("Invalid backend"); let format = matches - .opt_str(FORMAT) - .as_deref() - .map(|format| AudioFormat::from_str(format).expect("Invalid output format")) + .opt_str("format") + .as_ref() + .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) .unwrap_or_default(); - let device = matches.opt_str(DEVICE); + let device = matches.opt_str("device"); if device == Some("?".into()) { backend(device, format); exit(0); } - let mixer_name = matches.opt_str(MIXER_NAME); - let mixer = mixer::find(mixer_name.as_deref()).expect("Invalid mixer"); + let mixer_name = matches.opt_str("mixer"); + let mixer = mixer::find(mixer_name.as_ref()).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 index = matches - .opt_str(MIXER_INDEX) + let mixer_config = MixerConfig { + card: matches + .opt_str("mixer-card") + .unwrap_or_else(|| String::from("default")), + mixer: matches + .opt_str("mixer-name") + .unwrap_or_else(|| String::from("PCM")), + 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); - let mut volume_range = matches - .opt_str(VOLUME_RANGE) - .map(|range| range.parse::().unwrap()) - .unwrap_or_else(|| match mixer_name.as_deref() { - #[cfg(feature = "alsa-backend")] - 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") - }) - .unwrap_or_else(|| { - let mut volume_ctrl = VolumeCtrl::default(); - volume_ctrl.set_db_range(volume_range); - volume_ctrl - }); - - MixerConfig { - card, - control, - index, - volume_ctrl, - } + .unwrap_or(0), + mapped_volume: !matches.opt_present("mixer-linear-volume"), }; let cache = { let audio_dir; let system_dir; - if matches.opt_present(DISABLE_AUDIO_CACHE) { + if matches.opt_present("disable-audio-cache") { audio_dir = None; system_dir = matches - .opt_str(SYSTEM_CACHE) - .or_else(|| matches.opt_str(CACHE)) + .opt_str("system-cache") + .or_else(|| matches.opt_str("c")) .map(|p| p.into()); } else { - let cache_dir = matches.opt_str(CACHE); + let cache_dir = matches.opt_str("c"); audio_dir = cache_dir .as_ref() .map(|p| AsRef::::as_ref(p).join("files")); system_dir = matches - .opt_str(SYSTEM_CACHE) + .opt_str("system-cache") .or(cache_dir) .map(|p| p.into()); } 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| { @@ -554,28 +458,24 @@ fn get_setup(args: &[String]) -> Setup { }; let initial_volume = matches - .opt_str(INITIAL_VOLUME) - .map(|initial_volume| { - let volume = initial_volume.parse::().unwrap(); + .opt_str("initial-volume") + .map(|volume| { + let volume = 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 + panic!("Initial volume must be in the range 0-100"); } - (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 + (volume as i32 * 0xFFFF / 100) as u16 }) - .or_else(|| match mixer_name.as_deref() { - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => None, - _ => cache.as_ref().and_then(Cache::volume), - }); + .or_else(|| cache.as_ref().and_then(Cache::volume)) + .unwrap_or(0x8000); let zeroconf_port = matches - .opt_str(ZEROCONF_PORT) + .opt_str("zeroconf-port") .map(|port| port.parse::().unwrap()) .unwrap_or(0); let name = matches - .opt_str(NAME) + .opt_str("name") .unwrap_or_else(|| "Librespot".to_string()); let credentials = { @@ -588,8 +488,8 @@ fn get_setup(args: &[String]) -> Setup { }; get_credentials( - matches.opt_str(USERNAME), - matches.opt_str(PASSWORD), + matches.opt_str("username"), + matches.opt_str("password"), cached_credentials, password, ) @@ -601,12 +501,12 @@ 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: matches.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() { - panic!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); + panic!("Invalid proxy url, only urls on the format \"http://host:port\" are allowed"); } if url.scheme() != "http" { @@ -614,154 +514,123 @@ fn get_setup(args: &[String]) -> Setup { } 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 on the format \"http://host:port\" are allowed", err) } }, ), ap_port: matches - .opt_str(AP_PORT) + .opt_str("ap-port") .map(|port| port.parse::().expect("Invalid port")), } }; + let passthrough = matches.opt_present("passthrough"); + let player_config = { let bitrate = matches - .opt_str(BITRATE) - .as_deref() + .opt_str("b") + .as_ref() .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) .unwrap_or_default(); - - 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() + let gain_type = matches + .opt_str("normalisation-gain-type") + .as_ref() .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"), - ) + let normalisation_method = matches + .opt_str("normalisation-method") + .as_ref() + .map(|gain_type| { + NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method") }) - .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 ditherer_name = matches.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 format == AudioFormat::F64 || format == AudioFormat::F32 { - unimplemented!("Dithering is not available on format {:?}", format); - } - Some(dither::find_ditherer(ditherer_name).expect("Invalid ditherer")) - } - // nothing set on command line => use default - None => match format { - AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { - PlayerConfig::default().ditherer - } - _ => None, - }, - }; - - let passthrough = matches.opt_present(PASSTHROUGH); + .unwrap_or_default(); PlayerConfig { bitrate, - gapless, - passthrough, - normalisation, - normalisation_type, + gapless: !matches.opt_present("disable-gapless"), + normalisation: matches.opt_present("enable-volume-normalisation"), normalisation_method, - normalisation_pregain, - normalisation_threshold, - normalisation_attack, - normalisation_release, - normalisation_knee, - ditherer, + normalisation_type: gain_type, + normalisation_pregain: matches + .opt_str("normalisation-pregain") + .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) + .unwrap_or(PlayerConfig::default().normalisation_pregain), + normalisation_threshold: matches + .opt_str("normalisation-threshold") + .map(|threshold| { + NormalisationData::db_to_ratio( + threshold + .parse::() + .expect("Invalid threshold float value"), + ) + }) + .unwrap_or(PlayerConfig::default().normalisation_threshold), + normalisation_attack: matches + .opt_str("normalisation-attack") + .map(|attack| attack.parse::().expect("Invalid attack float value") / MILLIS) + .unwrap_or(PlayerConfig::default().normalisation_attack), + normalisation_release: matches + .opt_str("normalisation-release") + .map(|release| { + release.parse::().expect("Invalid release float value") / MILLIS + }) + .unwrap_or(PlayerConfig::default().normalisation_release), + normalisation_knee: matches + .opt_str("normalisation-knee") + .map(|knee| knee.parse::().expect("Invalid knee float value")) + .unwrap_or(PlayerConfig::default().normalisation_knee), + passthrough, } }; let connect_config = { let device_type = matches - .opt_str(DEVICE_TYPE) - .as_deref() + .opt_str("device-type") + .as_ref() .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); + + let volume_ctrl = matches + .opt_str("volume-ctrl") + .as_ref() + .map(|volume_ctrl| VolumeCtrl::from_str(volume_ctrl).expect("Invalid volume ctrl type")) + .unwrap_or_default(); ConnectConfig { name, device_type, - initial_volume, - has_volume_ctrl, - autoplay, + volume: initial_volume, + volume_ctrl, + autoplay: matches.opt_present("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); + let enable_discovery = !matches.opt_present("disable-discovery"); Setup { format, backend, - device, - mixer, cache, - player_config, session_config, + player_config, connect_config, - mixer_config, credentials, + device, enable_discovery, zeroconf_port, - player_event_program, - emit_sink_events, + mixer, + mixer_config, + player_event_program: matches.opt_str("onevent"), + emit_sink_events: matches.opt_present("emit-sink-events"), } } #[tokio::main(flavor = "current_thread")] async fn main() { - const RUST_BACKTRACE: &str = "RUST_BACKTRACE"; - if env::var(RUST_BACKTRACE).is_err() { - env::set_var(RUST_BACKTRACE, "full") + if env::var("RUST_BACKTRACE").is_err() { + env::set_var("RUST_BACKTRACE", "full") } let args: Vec = std::env::args().collect(); @@ -776,14 +645,11 @@ async fn main() { let mut connecting: Pin>> = Box::pin(future::pending()); if setup.enable_discovery { + let config = setup.connect_config.clone(); let device_id = setup.session_config.device_id.clone(); discovery = Some( - librespot::discovery::Discovery::builder(device_id) - .name(setup.connect_config.name.clone()) - .device_type(setup.connect_config.device_type) - .port(setup.zeroconf_port) - .launch() + librespot_connect::discovery::discovery(config, device_id, setup.zeroconf_port) .unwrap(), ); } @@ -831,7 +697,7 @@ async fn main() { session = &mut connecting, if !connecting.is_terminated() => match session { Ok(session) => { let mixer_config = setup.mixer_config.clone(); - let mixer = (setup.mixer)(mixer_config); + let mixer = (setup.mixer)(Some(mixer_config)); let player_config = setup.player_config.clone(); let connect_config = setup.connect_config.clone(); @@ -851,14 +717,14 @@ async fn main() { Ok(e) if e.success() => (), Ok(e) => { if let Some(code) = e.code() { - warn!("Sink event program returned exit code {}", code); + warn!("Sink event prog returned exit code {}", code); } else { - warn!("Sink event program returned failure"); + warn!("Sink event prog returned failure"); } - }, + } Err(e) => { warn!("Emitting sink event failed: {}", e); - }, + } } }))); } @@ -908,21 +774,13 @@ async fn main() { tokio::spawn(async move { match child.wait().await { - Ok(e) if e.success() => (), - Ok(e) => { - if let Some(code) = e.code() { - warn!("On event program returned exit code {}", code); - } else { - warn!("On event program returned failure"); - } - }, - Err(e) => { - warn!("On event program failed: {}", e); - }, + Ok(status) if !status.success() => error!("child exited with status {:?}", status.code()), + Err(e) => error!("failed to wait on child process: {}", e), + _ => {} } }); } else { - warn!("On event program failed to start"); + error!("program failed to start"); } } } From 69c2ad1387e9a47ae37e08342c10f10879f2a1f0 Mon Sep 17 00:00:00 2001 From: Nick Botticelli Date: Tue, 21 Sep 2021 01:18:58 -0700 Subject: [PATCH 021/147] 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 7ed8fc01eee472319c922e99476791668a57fb50 Mon Sep 17 00:00:00 2001 From: Nick Botticelli Date: Tue, 21 Sep 2021 02:13:32 -0700 Subject: [PATCH 022/147] Add more platforms to keyexchange.proto --- protocol/proto/keyexchange.proto | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/protocol/proto/keyexchange.proto b/protocol/proto/keyexchange.proto index 0907c912..840f5524 100644 --- a/protocol/proto/keyexchange.proto +++ b/protocol/proto/keyexchange.proto @@ -57,6 +57,23 @@ enum Platform { PLATFORM_ONKYO_ARM = 0x15; PLATFORM_QNXNTO_ARM = 0x16; PLATFORM_BCO_ARM = 0x17; + PLATFORM_WEBPLAYER = 0x18; + PLATFORM_WP8_ARM = 0x19; + PLATFORM_WP8_X86 = 0x1a; + PLATFORM_WINRT_ARM = 0x1b; + PLATFORM_WINRT_X86 = 0x1c; + PLATFORM_WINRT_X86_64 = 0x1d; + PLATFORM_FRONTIER = 0x1e; + PLATFORM_AMIGA_PPC = 0x1f; + PLATFORM_NANRADIO_NRX901 = 0x20; + PLATFORM_HARMAN_ARM = 0x21; + PLATFORM_SONY_PS3 = 0x22; + PLATFORM_SONY_PS4 = 0x23; + PLATFORM_IPHONE_ARM64 = 0x24; + PLATFORM_RTEMS_PPC = 0x25; + PLATFORM_GENERIC_PARTNER = 0x26; + PLATFORM_WIN32_X86_64 = 0x27; + PLATFORM_WATCHOS = 0x28; } enum Fingerprint { From 56585cabb6e4527ef3cd9f621a239d58550d42c7 Mon Sep 17 00:00:00 2001 From: Nick Botticelli Date: Tue, 21 Sep 2021 01:18:58 -0700 Subject: [PATCH 023/147] 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 024/147] 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 025/147] 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 026/147] 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 027/147] 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 028/147] 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 0e2686863aa0746f2e329f7c2220fb779a83d8d1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 7 Dec 2021 23:22:24 +0100 Subject: [PATCH 029/147] 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 030/147] 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 031/147] 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 032/147] 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 033/147] 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 034/147] 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 035/147] 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 036/147] 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 037/147] 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 038/147] 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 039/147] 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 040/147] 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 041/147] 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 2f7b9863d9c2862cf16890a755268bef3f3e50bb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 16 Dec 2021 22:42:37 +0100 Subject: [PATCH 042/147] 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 043/147] 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 044/147] 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 045/147] 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 046/147] 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 047/147] 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 048/147] 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 049/147] 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 050/147] 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 051/147] 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 052/147] 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 053/147] 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 054/147] 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 055/147] 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 056/147] 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 057/147] 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 058/147] 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 059/147] 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 060/147] 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", + )) } } } From afa2a021db7e8821687bd86ef5df1bfed7e7e938 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 29 Dec 2021 08:38:08 +0100 Subject: [PATCH 061/147] Enable CI for new-api --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30848c9b..aeb422cb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: test on: push: - branches: [master, dev] + branches: [dev, master, new-api] paths: [ "**.rs", @@ -31,6 +31,9 @@ on: "!LICENSE", "!*.sh", ] + schedule: + # Run CI every week + - cron: "00 01 * * 0" env: RUST_BACKTRACE: 1 From 3ce9854df5f16b6229bb3213340c90dbe2ff2963 Mon Sep 17 00:00:00 2001 From: Guillaume Desmottes Date: Tue, 28 Dec 2021 00:04:09 +0100 Subject: [PATCH 062/147] player: ensure load threads are done when dropping PlayerInternal Fix a race where the load operation was trying to use a disposed tokio context, resulting in panic. --- Cargo.lock | 1 + playback/Cargo.toml | 1 + playback/src/player.rs | 30 ++++++++++++++++++++++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 81f083ff..1e5bc36f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1406,6 +1406,7 @@ dependencies = [ "librespot-metadata", "log", "ogg", + "parking_lot", "portaudio-rs", "rand", "rand_distr", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index fee4dd51..92452d3c 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -26,6 +26,7 @@ shell-words = "1.0.0" thiserror = "1.0" tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } zerocopy = { version = "0.3" } +parking_lot = { version = "0.11", features = ["deadlock_detection"] } # Backends alsa = { version = "0.5", optional = true } diff --git a/playback/src/player.rs b/playback/src/player.rs index 747c4967..f7788fda 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,11 +1,13 @@ use std::{ cmp::max, + collections::HashMap, fmt, future::Future, io::{self, Read, Seek, SeekFrom}, mem, pin::Pin, process::exit, + sync::Arc, task::{Context, Poll}, thread, time::{Duration, Instant}, @@ -13,6 +15,7 @@ use std::{ use byteorder::{LittleEndian, ReadBytesExt}; use futures_util::{future, stream::futures_unordered::FuturesUnordered, StreamExt, TryFutureExt}; +use parking_lot::Mutex; use tokio::sync::{mpsc, oneshot}; use crate::{ @@ -56,6 +59,7 @@ struct PlayerInternal { session: Session, config: PlayerConfig, commands: mpsc::UnboundedReceiver, + load_handles: Arc>>>, state: PlayerState, preload: PlayerPreload, @@ -344,6 +348,7 @@ impl Player { session, config, commands: cmd_rx, + load_handles: Arc::new(Mutex::new(HashMap::new())), state: PlayerState::Stopped, preload: PlayerPreload::None, @@ -1953,7 +1958,7 @@ impl PlayerInternal { } fn load_track( - &self, + &mut self, spotify_id: SpotifyId, position_ms: u32, ) -> impl Future> + Send + 'static { @@ -1970,14 +1975,21 @@ impl PlayerInternal { let (result_tx, result_rx) = oneshot::channel(); + let load_handles_clone = self.load_handles.clone(); let handle = tokio::runtime::Handle::current(); - thread::spawn(move || { + let load_handle = 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); } + + let mut load_handles = load_handles_clone.lock(); + load_handles.remove(&thread::current().id()); }); + let mut load_handles = self.load_handles.lock(); + load_handles.insert(load_handle.thread().id(), load_handle); + result_rx.map_err(|_| ()) } @@ -2016,6 +2028,20 @@ impl PlayerInternal { impl Drop for PlayerInternal { fn drop(&mut self) { debug!("drop PlayerInternal[{}]", self.session.session_id()); + + let handles: Vec> = { + // waiting for the thread while holding the mutex would result in a deadlock + let mut load_handles = self.load_handles.lock(); + + load_handles + .drain() + .map(|(_thread_id, handle)| handle) + .collect() + }; + + for handle in handles { + let _ = handle.join(); + } } } From e51f475a00fac40ebecfc45ee9190335895dbe9f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 29 Dec 2021 22:18:38 +0100 Subject: [PATCH 063/147] Further initial loading improvements This should fix remaining cases of a client connecting, and failing to start playback from *beyond* the beginning when `librespot` is still loading that track. This undoes the `suppress_loading_status` workaround from #430, under the assumption that the race condition reported there has since been fixed on Spotify's end. --- Cargo.toml | 2 +- audio/src/fetch/receive.rs | 3 +- connect/src/spirc.rs | 67 +++++++++++++++++--------------------- playback/src/player.rs | 20 +++++------- 4 files changed, 42 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5a501ef5..3df50606 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", "parking_lot", "process"] } +tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" sha-1 = "0.9" diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index e04c58d2..270a62c8 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -427,7 +427,8 @@ pub(super) async fn audio_file_fetch( } None => break, } - } + }, + else => (), } if fetch.get_download_strategy() == DownloadStrategy::Streaming() { diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 144b9f24..d6692b51 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -550,7 +550,7 @@ impl SpircTask { SpircCommand::Play => { if active { self.handle_play(); - self.notify(None, true) + self.notify(None) } else { CommandSender::new(self, MessageType::kMessageTypePlay).send() } @@ -558,7 +558,7 @@ impl SpircTask { SpircCommand::PlayPause => { if active { self.handle_play_pause(); - self.notify(None, true) + self.notify(None) } else { CommandSender::new(self, MessageType::kMessageTypePlayPause).send() } @@ -566,7 +566,7 @@ impl SpircTask { SpircCommand::Pause => { if active { self.handle_pause(); - self.notify(None, true) + self.notify(None) } else { CommandSender::new(self, MessageType::kMessageTypePause).send() } @@ -574,7 +574,7 @@ impl SpircTask { SpircCommand::Prev => { if active { self.handle_prev(); - self.notify(None, true) + self.notify(None) } else { CommandSender::new(self, MessageType::kMessageTypePrev).send() } @@ -582,7 +582,7 @@ impl SpircTask { SpircCommand::Next => { if active { self.handle_next(); - self.notify(None, true) + self.notify(None) } else { CommandSender::new(self, MessageType::kMessageTypeNext).send() } @@ -590,7 +590,7 @@ impl SpircTask { SpircCommand::VolumeUp => { if active { self.handle_volume_up(); - self.notify(None, true) + self.notify(None) } else { CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send() } @@ -598,7 +598,7 @@ impl SpircTask { SpircCommand::VolumeDown => { if active { self.handle_volume_down(); - self.notify(None, true) + self.notify(None) } else { CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send() } @@ -629,7 +629,7 @@ impl SpircTask { PlayerEvent::Loading { .. } => { trace!("==> kPlayStatusLoading"); self.state.set_status(PlayStatus::kPlayStatusLoading); - self.notify(None, false) + self.notify(None) } PlayerEvent::Playing { position_ms, .. } => { trace!("==> kPlayStatusPlay"); @@ -642,7 +642,7 @@ 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) } else { Ok(()) } @@ -655,7 +655,7 @@ impl SpircTask { nominal_start_time: new_nominal_start_time, preloading_of_next_track_triggered: false, }; - self.notify(None, true) + self.notify(None) } _ => Ok(()), } @@ -673,7 +673,7 @@ 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) } else { Ok(()) } @@ -686,7 +686,7 @@ impl SpircTask { position_ms: new_position_ms, preloading_of_next_track_triggered: false, }; - self.notify(None, true) + self.notify(None) } _ => Ok(()), } @@ -699,7 +699,7 @@ impl SpircTask { warn!("The player has stopped unexpectedly."); self.state.set_status(PlayStatus::kPlayStatusStop); self.play_status = SpircPlayStatus::Stopped; - self.notify(None, true) + self.notify(None) } } } @@ -788,7 +788,7 @@ impl SpircTask { } match update.get_typ() { - MessageType::kMessageTypeHello => self.notify(Some(ident), true), + MessageType::kMessageTypeHello => self.notify(Some(ident)), MessageType::kMessageTypeLoad => { if !self.device.get_is_active() { @@ -810,47 +810,47 @@ impl SpircTask { self.play_status = SpircPlayStatus::Stopped; } - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypePlay => { self.handle_play(); - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypePlayPause => { self.handle_play_pause(); - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypePause => { self.handle_pause(); - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypeNext => { self.handle_next(); - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypePrev => { self.handle_prev(); - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypeVolumeUp => { self.handle_volume_up(); - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypeVolumeDown => { self.handle_volume_down(); - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypeRepeat => { self.state.set_repeat(update.get_state().get_repeat()); - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypeShuffle => { @@ -870,17 +870,16 @@ impl SpircTask { let context = self.state.get_context_uri(); debug!("{:?}", context); } - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypeSeek => { self.handle_seek(update.get_position()); - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypeReplace => { self.update_tracks(&update); - self.notify(None, true)?; if let SpircPlayStatus::Playing { preloading_of_next_track_triggered, @@ -898,12 +897,13 @@ impl SpircTask { } } } - Ok(()) + + self.notify(None) } MessageType::kMessageTypeVolume => { self.set_volume(update.get_volume() as u16); - self.notify(None, true) + self.notify(None) } MessageType::kMessageTypeNotify => { @@ -1172,7 +1172,7 @@ impl SpircTask { fn handle_end_of_track(&mut self) -> Result<(), Error> { self.handle_next(); - self.notify(None, true) + self.notify(None) } fn position(&mut self) -> u32 { @@ -1391,14 +1391,7 @@ impl SpircTask { CommandSender::new(self, MessageType::kMessageTypeHello).send() } - fn notify( - &mut self, - recipient: Option<&str>, - suppress_loading_status: bool, - ) -> Result<(), Error> { - if suppress_loading_status && (self.state.get_status() == PlayStatus::kPlayStatusLoading) { - return Ok(()); - }; + fn notify(&mut self, recipient: Option<&str>) -> Result<(), Error> { let status_string = match self.state.get_status() { PlayStatus::kPlayStatusLoading => "kPlayStatusLoading", PlayStatus::kPlayStatusPause => "kPlayStatusPause", diff --git a/playback/src/player.rs b/playback/src/player.rs index f7788fda..6e2933b2 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -857,14 +857,6 @@ impl PlayerTrackLoader { let stream_loader_controller = encrypted_file.get_stream_loader_controller().ok()?; - if play_from_beginning { - // No need to seek -> we stream from the beginning - stream_loader_controller.set_stream_mode(); - } else { - // we need to seek -> we set stream mode after the initial seek. - stream_loader_controller.set_random_access_mode(); - } - let key = match self.session.audio_key().request(spotify_id, file_id).await { Ok(key) => key, Err(e) => { @@ -875,6 +867,10 @@ impl PlayerTrackLoader { let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); + // Parsing normalisation data and starting playback from *beyond* the beginning + // will trigger a seek() so always start in random access mode. + stream_loader_controller.set_random_access_mode(); + let normalisation_data = match NormalisationData::parse_from_file(&mut decrypted_file) { Ok(data) => data, Err(_) => { @@ -930,15 +926,17 @@ impl PlayerTrackLoader { let mut stream_position_pcm = 0; let position_pcm = PlayerInternal::position_ms_to_pcm(position_ms); - if position_pcm > 0 { - stream_loader_controller.set_random_access_mode(); + if !play_from_beginning { 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(); }; + // Transition from random access mode to streaming mode now that + // we are ready to play from the requested position. + stream_loader_controller.set_stream_mode(); + info!("<{}> ({} ms) loaded", audio.name, audio.duration); return Some(PlayerLoadedTrackData { From 9b6e02fa0d4ea10c48ea575df39eb43846738ff6 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 29 Dec 2021 23:15:08 +0100 Subject: [PATCH 064/147] Prevent a few potential panics --- audio/src/fetch/receive.rs | 2 +- core/src/authentication.rs | 6 ++++++ core/src/dealer/mod.rs | 1 + discovery/src/server.rs | 9 +++++++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 270a62c8..b3d97eb4 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -418,7 +418,7 @@ pub(super) async fn audio_file_fetch( None => break, } } - data = file_data_rx.recv() => { + data = file_data_rx.recv() => { match data { Some(data) => { if fetch.handle_file_data(data)? == ControlFlow::Break { diff --git a/core/src/authentication.rs b/core/src/authentication.rs index ad7cf331..a4d34e2b 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -15,6 +15,8 @@ use crate::{protocol::authentication::AuthenticationType, Error}; pub enum AuthenticationError { #[error("unknown authentication type {0}")] AuthType(u32), + #[error("invalid key")] + Key, } impl From for Error { @@ -90,6 +92,10 @@ impl Credentials { let key = { let mut key = [0u8; 24]; + if key.len() < 20 { + return Err(AuthenticationError::Key.into()); + } + pbkdf2::>(&secret, username.as_bytes(), 0x100, &mut key[0..20]); let hash = &Sha1::digest(&key[..20]); diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index c1a9c94d..d598e6df 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -448,6 +448,7 @@ async fn connect( e = keep_flushing(&mut ws_tx) => { break Err(e) } + else => (), } }; diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 74af6fa3..4a251ea5 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -107,9 +107,14 @@ impl RequestHandler { let client_key = base64::decode(client_key.as_bytes())?; let shared_key = self.keys.shared_secret(&client_key); + let encrypted_blob_len = encrypted_blob.len(); + if encrypted_blob_len < 16 { + return Err(DiscoveryError::HmacError(encrypted_blob.to_vec()).into()); + } + let iv = &encrypted_blob[0..16]; - let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; - let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; + let encrypted = &encrypted_blob[16..encrypted_blob_len - 20]; + let cksum = &encrypted_blob[encrypted_blob_len - 20..encrypted_blob_len]; let base_key = Sha1::digest(&shared_key); let base_key = &base_key[..16]; From 05c768f6120f0d5e47c8c290ebf3aa1b30a613c3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 30 Dec 2021 20:52:49 +0100 Subject: [PATCH 065/147] Add audio preview, image and head file support --- core/src/session.rs | 10 ++++++++ core/src/spclient.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/core/src/session.rs b/core/src/session.rs index f1136e53..6b5a06df 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -373,6 +373,16 @@ impl Session { self.0.data.write().user_data.attributes.extend(attributes) } + pub fn get_user_attribute(&self, key: &str) -> Option { + self.0 + .data + .read() + .user_data + .attributes + .get(key) + .map(Clone::clone) + } + fn weak(&self) -> SessionWeak { SessionWeak(Arc::downgrade(&self.0)) } diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 1adfa3f8..addb547d 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -10,6 +10,7 @@ use hyper::{ }; use protobuf::Message; use rand::Rng; +use thiserror::Error; use crate::{ apresolve::SocketAddress, @@ -31,6 +32,18 @@ component! { pub type SpClientResult = Result; +#[derive(Debug, Error)] +pub enum SpClientError { + #[error("missing attribute {0}")] + Attribute(String), +} + +impl From for Error { + fn from(err: SpClientError) -> Self { + Self::failed_precondition(err) + } +} + #[derive(Copy, Clone, Debug)] pub enum RequestStrategy { TryTimes(usize), @@ -290,4 +303,50 @@ impl SpClient { Ok(stream) } + + pub async fn request_url(&self, url: String) -> SpClientResult { + let request = Request::builder() + .method(&Method::GET) + .uri(url) + .body(Body::empty())?; + + self.session().http_client().request_body(request).await + } + + // Audio preview in 96 kbps MP3, unencrypted + pub async fn get_audio_preview(&self, preview_id: &FileId) -> SpClientResult { + let attribute = "audio-preview-url-template"; + let template = self + .session() + .get_user_attribute(attribute) + .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?; + + let url = template.replace("{id}", &preview_id.to_base16()); + + self.request_url(url).await + } + + // The first 128 kB of a track, unencrypted + pub async fn get_head_file(&self, file_id: FileId) -> SpClientResult { + let attribute = "head-files-url"; + let template = self + .session() + .get_user_attribute(attribute) + .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?; + + let url = template.replace("{file_id}", &file_id.to_base16()); + + self.request_url(url).await + } + + pub async fn get_image(&self, image_id: FileId) -> SpClientResult { + let attribute = "image-url"; + let template = self + .session() + .get_user_attribute(attribute) + .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?; + let url = template.replace("{file_id}", &image_id.to_base16()); + + self.request_url(url).await + } } From 286a031d947d1636a705fc594d0574321a6fd0f6 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 30 Dec 2021 21:52:15 +0100 Subject: [PATCH 066/147] Always seek to starting position --- audio/src/fetch/mod.rs | 4 ++++ playback/src/player.rs | 21 +++++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index f9e85d10..2f478bba 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -387,6 +387,10 @@ impl AudioFileStreaming { bytes_per_second: usize, play_from_beginning: bool, ) -> Result { + // When the audio file is really small, this `download_size` may turn out to be + // larger than the audio file we're going to stream later on. This is OK; requesting + // `Content-Range` > `Content-Length` will return the complete file with status code + // 206 Partial Content. let download_size = if play_from_beginning { INITIAL_DOWNLOAD_SIZE + max( diff --git a/playback/src/player.rs b/playback/src/player.rs index 6e2933b2..218f5e05 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -867,8 +867,7 @@ impl PlayerTrackLoader { let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); - // Parsing normalisation data and starting playback from *beyond* the beginning - // will trigger a seek() so always start in random access mode. + // Parsing normalisation data will trigger a seek() so always start in random access mode. stream_loader_controller.set_random_access_mode(); let normalisation_data = match NormalisationData::parse_from_file(&mut decrypted_file) { @@ -923,13 +922,19 @@ impl PlayerTrackLoader { } }; - let mut stream_position_pcm = 0; + // Ensure the starting position. Even when we want to play from the beginning, + // the cursor may have been moved by parsing normalisation data. This may not + // matter for playback (but won't hurt either), but may be useful for the + // passthrough decoder. let position_pcm = PlayerInternal::position_ms_to_pcm(position_ms); - - if !play_from_beginning { - match decoder.seek(position_pcm) { - Ok(_) => stream_position_pcm = position_pcm, - Err(e) => error!("PlayerTrackLoader::load_track error seeking: {}", e), + let stream_position_pcm = match decoder.seek(position_pcm) { + Ok(_) => position_pcm, + Err(e) => { + warn!( + "PlayerTrackLoader::load_track error seeking to PCM page {}: {}", + position_pcm, e + ); + 0 } }; From 2af34fc674a9e5c1c0c844a18d2908e75b615e17 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 30 Dec 2021 22:36:38 +0100 Subject: [PATCH 067/147] Add product metrics to requests --- core/src/http_client.rs | 2 +- core/src/session.rs | 7 +++++++ core/src/spclient.rs | 8 ++++++++ metadata/src/request.rs | 16 +++++++++++++++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 1cdfcf75..e445b953 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -129,7 +129,7 @@ impl HttpClient { } pub async fn request(&self, req: Request) -> Result, Error> { - debug!("Requesting {:?}", req.uri().to_string()); + debug!("Requesting {}", req.uri().to_string()); let request = self.request_fut(req)?; let response = request.await; diff --git a/core/src/session.rs b/core/src/session.rs index 6b5a06df..aecdaada 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -334,6 +334,9 @@ impl Session { &self.0.config } + // This clones a fairly large struct, so use a specific getter or setter unless + // you need more fields at once, in which case this can spare multiple `read` + // locks. pub fn user_data(&self) -> UserData { self.0.data.read().user_data.clone() } @@ -354,6 +357,10 @@ impl Session { self.0.data.read().user_data.canonical_username.clone() } + pub fn country(&self) -> String { + self.0.data.read().user_data.country.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()); diff --git a/core/src/spclient.rs b/core/src/spclient.rs index addb547d..ffc2ebba 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -136,6 +136,14 @@ impl SpClient { let mut url = self.base_url().await; url.push_str(endpoint); + // Add metrics. There is also an optional `partner` key with a value like + // `vodafone-uk` but we've yet to discover how we can find that value. + let separator = match url.find('?') { + Some(_) => "&", + None => "?", + }; + url.push_str(&format!("{}product=0", separator)); + let mut request = Request::builder() .method(method) .uri(url) diff --git a/metadata/src/request.rs b/metadata/src/request.rs index 2ebd4037..df276722 100644 --- a/metadata/src/request.rs +++ b/metadata/src/request.rs @@ -7,7 +7,21 @@ pub type RequestResult = Result; #[async_trait] pub trait MercuryRequest { async fn request(session: &Session, uri: &str) -> RequestResult { - let request = session.mercury().get(uri)?; + let mut metrics_uri = uri.to_owned(); + + let separator = match metrics_uri.find('?') { + Some(_) => "&", + None => "?", + }; + metrics_uri.push_str(&format!("{}country={}", separator, session.country())); + + if let Some(product) = session.get_user_attribute("type") { + metrics_uri.push_str(&format!("&product={}", product)); + } + + trace!("Requesting {}", metrics_uri); + + let request = session.mercury().get(metrics_uri)?; let response = request.await?; match response.payload.first() { Some(data) => { From 0fdff0d3fd2f8a10e33af14a7376d48fbac9b3ac Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 30 Dec 2021 23:50:28 +0100 Subject: [PATCH 068/147] Follow client setting to filter explicit tracks - Don't load explicit tracks when the client setting forbids them - When a client switches explicit filtering on *while* playing an explicit track, immediately skip to the next track --- connect/src/spirc.rs | 5 ++++ metadata/src/audio/item.rs | 1 + metadata/src/episode.rs | 1 + metadata/src/track.rs | 1 + playback/src/player.rs | 55 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index d6692b51..864031f6 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -746,12 +746,17 @@ impl SpircTask { _ => old_value, }; self.session.set_user_attribute(key, new_value); + trace!( "Received attribute mutation, {} was {} is now {}", key, old_value, new_value ); + + if key == "filter-explicit-content" && new_value == "1" { + self.player.skip_explicit_content(); + } } else { trace!( "Received attribute mutation for {} but key was not found!", diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index 2b1f4eba..89860c04 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -26,6 +26,7 @@ pub struct AudioItem { pub duration: i32, pub availability: AudioItemAvailability, pub alternatives: Option, + pub is_explicit: bool, } impl AudioItem { diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 30aae5a8..0eda76ff 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -80,6 +80,7 @@ impl InnerAudioItem for Episode { duration: episode.duration, availability, alternatives: None, + is_explicit: episode.is_explicit, }) } } diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 06efd310..df1db8d1 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -95,6 +95,7 @@ impl InnerAudioItem for Track { duration: track.duration, availability, alternatives, + is_explicit: track.is_explicit, }) } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 218f5e05..211e1795 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -98,6 +98,7 @@ enum PlayerCommand { SetSinkEventCallback(Option), EmitVolumeSetEvent(u16), SetAutoNormaliseAsAlbum(bool), + SkipExplicitContent(), } #[derive(Debug, Clone)] @@ -456,6 +457,10 @@ impl Player { pub fn set_auto_normalise_as_album(&self, setting: bool) { self.command(PlayerCommand::SetAutoNormaliseAsAlbum(setting)); } + + pub fn skip_explicit_content(&self) { + self.command(PlayerCommand::SkipExplicitContent()); + } } impl Drop for Player { @@ -478,6 +483,7 @@ struct PlayerLoadedTrackData { bytes_per_second: usize, duration_ms: u32, stream_position_pcm: u64, + is_explicit: bool, } enum PlayerPreload { @@ -513,6 +519,7 @@ enum PlayerState { duration_ms: u32, stream_position_pcm: u64, suggested_to_preload_next_track: bool, + is_explicit: bool, }, Playing { track_id: SpotifyId, @@ -526,6 +533,7 @@ enum PlayerState { stream_position_pcm: u64, reported_nominal_start_time: Option, suggested_to_preload_next_track: bool, + is_explicit: bool, }, EndOfTrack { track_id: SpotifyId, @@ -608,6 +616,7 @@ impl PlayerState { normalisation_data, stream_loader_controller, stream_position_pcm, + is_explicit, .. } => { *self = EndOfTrack { @@ -620,6 +629,7 @@ impl PlayerState { bytes_per_second, duration_ms, stream_position_pcm, + is_explicit, }, }; } @@ -648,6 +658,7 @@ impl PlayerState { bytes_per_second, stream_position_pcm, suggested_to_preload_next_track, + is_explicit, } => { *self = Playing { track_id, @@ -661,6 +672,7 @@ impl PlayerState { stream_position_pcm, reported_nominal_start_time: None, suggested_to_preload_next_track, + is_explicit, }; } _ => { @@ -689,6 +701,7 @@ impl PlayerState { stream_position_pcm, reported_nominal_start_time: _, suggested_to_preload_next_track, + is_explicit, } => { *self = Paused { track_id, @@ -701,6 +714,7 @@ impl PlayerState { bytes_per_second, stream_position_pcm, suggested_to_preload_next_track, + is_explicit, }; } _ => { @@ -778,6 +792,16 @@ impl PlayerTrackLoader { audio.name, audio.spotify_uri ); + let is_explicit = audio.is_explicit; + if is_explicit { + if let Some(value) = self.session.get_user_attribute("filter-explicit-content") { + if &value == "1" { + warn!("Track is marked as explicit, which client setting forbids."); + return None; + } + } + } + let audio = match self.find_available_alternative(audio).await { Some(audio) => audio, None => { @@ -951,6 +975,7 @@ impl PlayerTrackLoader { bytes_per_second, duration_ms, stream_position_pcm, + is_explicit, }); } } @@ -1518,6 +1543,7 @@ impl PlayerInternal { Instant::now() - Duration::from_millis(position_ms as u64), ), suggested_to_preload_next_track: false, + is_explicit: loaded_track.is_explicit, }; } else { self.ensure_sink_stopped(false); @@ -1533,6 +1559,7 @@ impl PlayerInternal { bytes_per_second: loaded_track.bytes_per_second, stream_position_pcm: loaded_track.stream_position_pcm, suggested_to_preload_next_track: false, + is_explicit: loaded_track.is_explicit, }; self.send_event(PlayerEvent::Paused { @@ -1674,6 +1701,7 @@ impl PlayerInternal { bytes_per_second, duration_ms, normalisation_data, + is_explicit, .. } | PlayerState::Paused { @@ -1683,6 +1711,7 @@ impl PlayerInternal { bytes_per_second, duration_ms, normalisation_data, + is_explicit, .. } = old_state { @@ -1693,6 +1722,7 @@ impl PlayerInternal { bytes_per_second, duration_ms, stream_position_pcm, + is_explicit, }; self.preload = PlayerPreload::None; @@ -1943,6 +1973,30 @@ impl PlayerInternal { PlayerCommand::SetAutoNormaliseAsAlbum(setting) => { self.auto_normalise_as_album = setting } + + PlayerCommand::SkipExplicitContent() => { + if let PlayerState::Playing { + track_id, + play_request_id, + is_explicit, + .. + } + | PlayerState::Paused { + track_id, + play_request_id, + is_explicit, + .. + } = self.state + { + if is_explicit { + warn!("Currently loaded track is explicit, which client setting forbids -- skipping to next track."); + self.send_event(PlayerEvent::EndOfTrack { + track_id, + play_request_id, + }) + } + } + } }; Ok(result) @@ -2080,6 +2134,7 @@ impl fmt::Debug for PlayerCommand { .debug_tuple("SetAutoNormaliseAsAlbum") .field(&setting) .finish(), + PlayerCommand::SkipExplicitContent() => f.debug_tuple("SkipExplicitContent").finish(), } } } From 2d699e288a43d4eb54dd451e6d66d8e6b0cc3c93 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 1 Jan 2022 20:23:21 +0100 Subject: [PATCH 069/147] Follow autoplay client setting --- connect/src/spirc.rs | 35 +++++++++++++++++++++-------------- core/src/config.rs | 2 -- src/main.rs | 9 --------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 864031f6..eedb6cbd 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -86,7 +86,6 @@ type BoxedStream = Pin + Send>>; struct SpircTask { player: Player, mixer: Box, - config: SpircTaskConfig, sequence: SeqGenerator, @@ -123,10 +122,6 @@ pub enum SpircCommand { Shuffle, } -struct SpircTaskConfig { - autoplay: bool, -} - const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; @@ -337,9 +332,6 @@ impl Spirc { let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); let initial_volume = config.initial_volume; - let task_config = SpircTaskConfig { - autoplay: config.autoplay, - }; let device = initial_device_state(config); @@ -348,7 +340,6 @@ impl Spirc { let mut task = SpircTask { player, mixer, - config: task_config, sequence: SeqGenerator::new(1), @@ -1098,8 +1089,19 @@ impl SpircTask { self.context_fut = self.resolve_station(&context_uri); self.update_tracks_from_context(); } + if new_index >= tracks_len { - if self.config.autoplay { + let autoplay = self + .session + .get_user_attribute("autoplay") + .unwrap_or_else(|| { + warn!( + "Unable to get autoplay user attribute. Continuing with autoplay disabled." + ); + "0".into() + }); + + if autoplay == "1" { // Extend the playlist debug!("Extending playlist <{}>", context_uri); self.update_tracks_from_context(); @@ -1262,18 +1264,23 @@ impl SpircTask { fn update_tracks(&mut self, frame: &protocol::spirc::Frame) { 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(); + trace!("Frame has {:?} tracks", tracks.len()); + if context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:") { self.context_fut = self.resolve_station(&context_uri); - } else if self.config.autoplay { - info!("Fetching autoplay context uri"); - // Get autoplay_station_uri for regular playlists - self.autoplay_fut = self.resolve_autoplay_uri(&context_uri); + } else if let Some(autoplay) = self.session.get_user_attribute("autoplay") { + if &autoplay == "1" { + info!("Fetching autoplay context uri"); + // Get autoplay_station_uri for regular playlists + self.autoplay_fut = self.resolve_autoplay_uri(&context_uri); + } } self.player diff --git a/core/src/config.rs b/core/src/config.rs index f04326ae..b667a330 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -123,7 +123,6 @@ pub struct ConnectConfig { pub device_type: DeviceType, pub initial_volume: Option, pub has_volume_ctrl: bool, - pub autoplay: bool, } impl Default for ConnectConfig { @@ -133,7 +132,6 @@ impl Default for ConnectConfig { device_type: DeviceType::default(), initial_volume: Some(50), has_volume_ctrl: true, - autoplay: false, } } } diff --git a/src/main.rs b/src/main.rs index 07952e5e..8f2e532c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -201,7 +201,6 @@ fn get_setup() -> Setup { 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 = "bitrate"; const CACHE: &str = "cache"; @@ -245,7 +244,6 @@ fn get_setup() -> Setup { 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"; @@ -376,11 +374,6 @@ fn get_setup() -> Setup { 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, @@ -1245,14 +1238,12 @@ fn get_setup() -> Setup { .unwrap_or_default(); let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); - let autoplay = opt_present(AUTOPLAY); ConnectConfig { name, device_type, initial_volume, has_volume_ctrl, - autoplay, } }; From 7921f239276099ac1175233fc45252e14030ea52 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 3 Jan 2022 00:13:28 +0100 Subject: [PATCH 070/147] Improve format handling and support MP3 - Switch from `lewton` to `Symphonia`. This is a pure Rust demuxer and decoder in active development that supports a wide range of formats, including Ogg Vorbis, MP3, AAC and FLAC for future HiFi support. At the moment only Ogg Vorbis and MP3 are enabled; all AAC files are DRM-protected. - Bump MSRV to 1.51, required for `Symphonia`. - Filter out all files whose format is not specified. - Not all episodes seem to be encrypted. If we can't get an audio key, try and see if we can play the file without decryption. - After seeking, report the actual position instead of the target. - Remove the 0xa7 bytes offset from `Subfile`, `Symphonia` does not balk at Spotify's custom Ogg packet before it. This also simplifies handling of formats other than Ogg Vorbis. - When there is no next track to load, signal the UI that the player has stopped. Before, the player would get stuck in an infinite reloading loop when there was only one track in the queue and that track could not be loaded. --- Cargo.lock | 119 +++++++++- audio/src/decrypt.rs | 24 +- audio/src/fetch/mod.rs | 1 + audio/src/lib.rs | 4 +- connect/src/spirc.rs | 34 ++- metadata/src/audio/file.rs | 10 +- playback/Cargo.toml | 10 +- playback/src/audio_backend/mod.rs | 2 +- playback/src/decoder/lewton_decoder.rs | 4 +- playback/src/decoder/mod.rs | 62 ++++-- playback/src/decoder/passthrough_decoder.rs | 33 ++- playback/src/decoder/symphonia_decoder.rs | 229 ++++++++++++-------- playback/src/player.rs | 153 +++++++------ 13 files changed, 445 insertions(+), 240 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e5bc36f..a7f5093d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,12 @@ version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "async-trait" version = "0.1.51" @@ -186,6 +192,12 @@ version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +[[package]] +name = "bytemuck" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" + [[package]] name = "byteorder" version = "1.4.3" @@ -446,6 +458,15 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encoding_rs" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -1121,17 +1142,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] - [[package]] name = "libc" version = "0.2.109" @@ -1398,7 +1408,6 @@ dependencies = [ "gstreamer", "gstreamer-app", "jack", - "lewton", "libpulse-binding", "libpulse-simple-binding", "librespot-audio", @@ -1413,6 +1422,7 @@ dependencies = [ "rodio", "sdl2", "shell-words", + "symphonia", "thiserror", "tokio", "zerocopy", @@ -2470,6 +2480,91 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "symphonia" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e5f38aa07e792f4eebb0faa93cee088ec82c48222dd332897aae1569d9a4b7" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-ogg", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4d97c4a61ece4651751dddb393ebecb7579169d9e758ae808fe507a5250790" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a29ed6748078effb35a05064a451493a78038918981dc1a76bdf5a2752d441fa" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa135e97be0f4a666c31dfe5ef4c75435ba3d355fd6a73d2100aa79b14c104c9" +dependencies = [ + "arrayvec", + "bitflags", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b2357288a79adfec532cfd86049696cfa5c58efeff83bd51687a528f18a519" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-metadata" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5260599daba18d8fe905ca3eb3b42ba210529a6276886632412cc74984e79b1a" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37026c6948ff842e0bf94b4008579cc71ab16ed0ff9ca70a331f60f4f1e1e9" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.82" diff --git a/audio/src/decrypt.rs b/audio/src/decrypt.rs index 95dc7c08..e11241a9 100644 --- a/audio/src/decrypt.rs +++ b/audio/src/decrypt.rs @@ -14,16 +14,20 @@ const AUDIO_AESIV: [u8; 16] = [ ]; pub struct AudioDecrypt { - cipher: Aes128Ctr, + // a `None` cipher is a convenience to make `AudioDecrypt` pass files unaltered + cipher: Option, reader: T, } impl AudioDecrypt { - pub fn new(key: AudioKey, reader: T) -> AudioDecrypt { - let cipher = Aes128Ctr::new( - GenericArray::from_slice(&key.0), - GenericArray::from_slice(&AUDIO_AESIV), - ); + pub fn new(key: Option, reader: T) -> AudioDecrypt { + let cipher = key.map(|key| { + Aes128Ctr::new( + GenericArray::from_slice(&key.0), + GenericArray::from_slice(&AUDIO_AESIV), + ) + }); + AudioDecrypt { cipher, reader } } } @@ -32,7 +36,9 @@ impl io::Read for AudioDecrypt { fn read(&mut self, output: &mut [u8]) -> io::Result { let len = self.reader.read(output)?; - self.cipher.apply_keystream(&mut output[..len]); + if let Some(ref mut cipher) = self.cipher { + cipher.apply_keystream(&mut output[..len]); + } Ok(len) } @@ -42,7 +48,9 @@ impl io::Seek for AudioDecrypt { fn seek(&mut self, pos: io::SeekFrom) -> io::Result { let newpos = self.reader.seek(pos)?; - self.cipher.seek(newpos); + if let Some(ref mut cipher) = self.cipher { + cipher.seek(newpos); + } Ok(newpos) } diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 2f478bba..f3229574 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -57,6 +57,7 @@ impl From for Error { /// 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. +/// The Symphonia decoder requires this to be a power of 2 and > 32 kB. /// Note: smaller requests can happen if part of the block is downloaded already. pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128; diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 5685486d..22bf2f0a 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -9,6 +9,6 @@ mod range_set; pub use decrypt::AudioDecrypt; 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, + MINIMUM_DOWNLOAD_SIZE, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, + READ_AHEAD_DURING_PLAYBACK, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, }; diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index eedb6cbd..427555ff 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -687,7 +687,6 @@ impl SpircTask { 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) @@ -801,9 +800,7 @@ impl SpircTask { 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); - self.player.stop(); - self.play_status = SpircPlayStatus::Stopped; + self.handle_stop(); } self.notify(None) @@ -909,9 +906,7 @@ impl SpircTask { <= 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; + self.handle_stop(); } Ok(()) } @@ -920,6 +915,10 @@ impl SpircTask { } } + fn handle_stop(&mut self) { + self.player.stop(); + } + fn handle_play(&mut self) { match self.play_status { SpircPlayStatus::Paused { @@ -1036,13 +1035,14 @@ impl SpircTask { .. } => { *preloading_of_next_track_triggered = true; - if let Some(track_id) = self.preview_next_track() { - self.player.preload(track_id); - } } - SpircPlayStatus::LoadingPause { .. } - | SpircPlayStatus::LoadingPlay { .. } - | SpircPlayStatus::Stopped => (), + _ => (), + } + + if let Some(track_id) = self.preview_next_track() { + self.player.preload(track_id); + } else { + self.handle_stop(); } } @@ -1122,9 +1122,7 @@ impl SpircTask { } else { info!("Not playing next track because there are no more tracks left in queue."); self.state.set_playing_track_index(0); - self.state.set_status(PlayStatus::kPlayStatusStop); - self.player.stop(); - self.play_status = SpircPlayStatus::Stopped; + self.handle_stop(); } } @@ -1392,9 +1390,7 @@ impl SpircTask { } } None => { - self.state.set_status(PlayStatus::kPlayStatusStop); - self.player.stop(); - self.play_status = SpircPlayStatus::Stopped; + self.handle_stop(); } } } diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs index d3ce69b7..65608814 100644 --- a/metadata/src/audio/file.rs +++ b/metadata/src/audio/file.rs @@ -20,7 +20,15 @@ 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()))) + .filter_map(|file| { + let file_id = FileId::from(file.get_file_id()); + if file.has_format() { + Some((file.get_format(), file_id)) + } else { + trace!("Ignoring file <{}> with unspecified format", file_id); + None + } + }) .collect(); AudioFiles(audio_files) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 92452d3c..262312c0 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -18,15 +18,15 @@ path = "../metadata" version = "0.3.1" [dependencies] +byteorder = "1.4" futures-executor = "0.3" futures-util = { version = "0.3", default_features = false, features = ["alloc"] } log = "0.4" -byteorder = "1.4" +parking_lot = { version = "0.11", features = ["deadlock_detection"] } shell-words = "1.0.0" thiserror = "1.0" tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } zerocopy = { version = "0.3" } -parking_lot = { version = "0.11", features = ["deadlock_detection"] } # Backends alsa = { version = "0.5", optional = true } @@ -43,8 +43,10 @@ glib = { version = "0.10", optional = true } rodio = { version = "0.14", optional = true, default-features = false } cpal = { version = "0.13", optional = true } -# Decoder -lewton = "0.10" +# Container and audio decoder +symphonia = { version = "0.4", default-features = false, features = ["mp3", "ogg", "vorbis"] } + +# Legacy Ogg container decoder for the passthrough decoder ogg = "0.8" # Dithering diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index dc21fb3d..aab43476 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -71,7 +71,7 @@ macro_rules! sink_as_bytes { self.write_bytes(samples_s16.as_bytes()) } }, - AudioPacket::OggData(samples) => self.write_bytes(samples), + AudioPacket::Raw(samples) => self.write_bytes(samples), } } }; diff --git a/playback/src/decoder/lewton_decoder.rs b/playback/src/decoder/lewton_decoder.rs index bc90b992..9e79c1e3 100644 --- a/playback/src/decoder/lewton_decoder.rs +++ b/playback/src/decoder/lewton_decoder.rs @@ -25,11 +25,11 @@ impl AudioDecoder for VorbisDecoder where R: Read + Seek, { - fn seek(&mut self, absgp: u64) -> DecoderResult<()> { + fn seek(&mut self, absgp: u64) -> Result { self.0 .seek_absgp_pg(absgp) .map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?; - Ok(()) + Ok(absgp) } fn next_packet(&mut self) -> DecoderResult> { diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index 087bba4c..c0ede5a0 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -1,26 +1,28 @@ use thiserror::Error; -mod lewton_decoder; -pub use lewton_decoder::VorbisDecoder; +use crate::metadata::audio::AudioFileFormat; mod passthrough_decoder; pub use passthrough_decoder::PassthroughDecoder; +mod symphonia_decoder; +pub use symphonia_decoder::SymphoniaDecoder; + #[derive(Error, Debug)] pub enum DecoderError { - #[error("Lewton Decoder Error: {0}")] - LewtonDecoder(String), #[error("Passthrough Decoder Error: {0}")] PassthroughDecoder(String), + #[error("Symphonia Decoder Error: {0}")] + SymphoniaDecoder(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")] + #[error("Decoder Raw Error: Can't return Raw on Samples")] + Raw, + #[error("Decoder Samples Error: Can't return Samples on Raw")] Samples, } @@ -28,25 +30,20 @@ pub type AudioPacketResult = Result; pub enum AudioPacket { Samples(Vec), - OggData(Vec), + Raw(Vec), } impl AudioPacket { - pub fn samples_from_f32(f32_samples: Vec) -> Self { - let f64_samples = f32_samples.iter().map(|sample| *sample as f64).collect(); - AudioPacket::Samples(f64_samples) - } - pub fn samples(&self) -> AudioPacketResult<&[f64]> { match self { AudioPacket::Samples(s) => Ok(s), - AudioPacket::OggData(_) => Err(AudioPacketError::OggData), + AudioPacket::Raw(_) => Err(AudioPacketError::Raw), } } pub fn oggdata(&self) -> AudioPacketResult<&[u8]> { match self { - AudioPacket::OggData(d) => Ok(d), + AudioPacket::Raw(d) => Ok(d), AudioPacket::Samples(_) => Err(AudioPacketError::Samples), } } @@ -54,12 +51,43 @@ impl AudioPacket { pub fn is_empty(&self) -> bool { match self { AudioPacket::Samples(s) => s.is_empty(), - AudioPacket::OggData(d) => d.is_empty(), + AudioPacket::Raw(d) => d.is_empty(), } } } pub trait AudioDecoder { - fn seek(&mut self, absgp: u64) -> DecoderResult<()>; + fn seek(&mut self, absgp: u64) -> Result; fn next_packet(&mut self) -> DecoderResult>; + + fn is_ogg_vorbis(format: AudioFileFormat) -> bool + where + Self: Sized, + { + matches!( + format, + AudioFileFormat::OGG_VORBIS_320 + | AudioFileFormat::OGG_VORBIS_160 + | AudioFileFormat::OGG_VORBIS_96 + ) + } + + fn is_mp3(format: AudioFileFormat) -> bool + where + Self: Sized, + { + matches!( + format, + AudioFileFormat::MP3_320 + | AudioFileFormat::MP3_256 + | AudioFileFormat::MP3_160 + | AudioFileFormat::MP3_96 + ) + } +} + +impl From for DecoderError { + fn from(err: symphonia::core::errors::Error) -> Self { + Self::SymphoniaDecoder(err.to_string()) + } } diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index dd8e3b32..9b8eedf8 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -1,8 +1,15 @@ // Passthrough decoder for librespot -use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; +use std::{ + io::{Read, Seek}, + time::{SystemTime, UNIX_EPOCH}, +}; + +// TODO: move this to the Symphonia Ogg demuxer use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; -use std::io::{Read, Seek}; -use std::time::{SystemTime, UNIX_EPOCH}; + +use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; + +use crate::metadata::audio::AudioFileFormat; fn get_header(code: u8, rdr: &mut PacketReader) -> DecoderResult> where @@ -36,7 +43,14 @@ pub struct PassthroughDecoder { impl PassthroughDecoder { /// Constructs a new Decoder from a given implementation of `Read + Seek`. - pub fn new(rdr: R) -> DecoderResult { + pub fn new(rdr: R, format: AudioFileFormat) -> DecoderResult { + if !Self::is_ogg_vorbis(format) { + return Err(DecoderError::PassthroughDecoder(format!( + "Passthrough decoder is not implemented for format {:?}", + format + ))); + } + let mut rdr = PacketReader::new(rdr); let since_epoch = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -68,7 +82,7 @@ impl PassthroughDecoder { } impl AudioDecoder for PassthroughDecoder { - fn seek(&mut self, absgp: u64) -> DecoderResult<()> { + fn seek(&mut self, absgp: u64) -> Result { // add an eos to previous stream if missing if self.bos && !self.eos { match self.rdr.read_packet() { @@ -101,9 +115,10 @@ impl AudioDecoder for PassthroughDecoder { .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(()) + let new_page = pck.absgp_page(); + self.ofsgp_page = new_page; + debug!("Seek to offset page {}", new_page); + Ok(new_page) } None => Err(DecoderError::PassthroughDecoder( "Packet is None".to_string(), @@ -184,7 +199,7 @@ impl AudioDecoder for PassthroughDecoder { let data = self.wtr.inner_mut(); if !data.is_empty() { - let ogg_data = AudioPacket::OggData(std::mem::take(data)); + let ogg_data = AudioPacket::Raw(std::mem::take(data)); return Ok(Some(ogg_data)); } } diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 309c495d..5546faa5 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -1,136 +1,173 @@ +use std::io; + +use symphonia::core::{ + audio::{SampleBuffer, SignalSpec}, + codecs::{Decoder, DecoderOptions}, + errors::Error, + formats::{FormatReader, SeekMode, SeekTo}, + io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions}, + meta::{MetadataOptions, StandardTagKey, Value}, + probe::Hint, +}; + 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) - } -} +use crate::{metadata::audio::AudioFileFormat, player::NormalisationData}; pub struct SymphoniaDecoder { track_id: u32, decoder: Box, format: Box, - position: TimeStamp, + sample_buffer: SampleBuffer, } impl SymphoniaDecoder { - pub fn new(input: R) -> DecoderResult + pub fn new(input: R, format: AudioFileFormat) -> DecoderResult where - R: Read + Seek, + R: MediaSource + 'static, { - let mss_opts = Default::default(); - let mss = MediaSourceStream::new(Box::new(FileWithConstSize::new(input)), mss_opts); + let mss_opts = MediaSourceStreamOptions { + buffer_len: librespot_audio::MINIMUM_DOWNLOAD_SIZE, + }; + let mss = MediaSourceStream::new(Box::new(input), mss_opts); + + // Not necessary, but speeds up loading. + let mut hint = Hint::new(); + if Self::is_ogg_vorbis(format) { + hint.with_extension("ogg"); + hint.mime_type("audio/ogg"); + } else if Self::is_mp3(format) { + hint.with_extension("mp3"); + hint.mime_type("audio/mp3"); + } let format_opts = Default::default(); - let format = OggReader::try_new(mss, &format_opts).map_err(|e| DecoderError::SymphoniaDecoder(e.to_string()))?; + let metadata_opts: MetadataOptions = Default::default(); + let decoder_opts: DecoderOptions = Default::default(); - let track = format.default_track().unwrap(); - let decoder_opts = Default::default(); - let decoder = VorbisDecoder::try_new(&track.codec_params, &decoder_opts)?; + let probed = + symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts)?; + let format = probed.format; + + let track = format.default_track().ok_or_else(|| { + DecoderError::SymphoniaDecoder("Could not retrieve default track".into()) + })?; + + let decoder = symphonia::default::get_codecs().make(&track.codec_params, &decoder_opts)?; + + let codec_params = decoder.codec_params(); + let rate = codec_params.sample_rate.ok_or_else(|| { + DecoderError::SymphoniaDecoder("Could not retrieve sample rate".into()) + })?; + let channels = codec_params.channels.ok_or_else(|| { + DecoderError::SymphoniaDecoder("Could not retrieve channel configuration".into()) + })?; + + if rate != crate::SAMPLE_RATE { + return Err(DecoderError::SymphoniaDecoder(format!( + "Unsupported sample rate: {}", + rate + ))); + } + + if channels.count() != crate::NUM_CHANNELS as usize { + return Err(DecoderError::SymphoniaDecoder(format!( + "Unsupported number of channels: {}", + channels + ))); + } + + // TODO: settle on a sane default depending on the format + let max_frames = decoder.codec_params().max_frames_per_packet.unwrap_or(8192); + let sample_buffer = SampleBuffer::new(max_frames, SignalSpec { rate, channels }); Ok(Self { track_id: track.id, - decoder: Box::new(decoder), - format: Box::new(format), - position: 0, + decoder, + format, + sample_buffer, }) } + + pub fn normalisation_data(&mut self) -> Option { + let mut metadata = self.format.metadata(); + loop { + if let Some(_discarded_revision) = metadata.pop() { + // Advance to the latest metadata revision. + continue; + } else { + let revision = metadata.current()?; + let tags = revision.tags(); + + if tags.is_empty() { + // The latest metadata entry in the log is empty. + return None; + } + + let mut data = NormalisationData::default(); + let mut i = 0; + while i < tags.len() { + if let Value::Float(value) = tags[i].value { + #[allow(non_snake_case)] + match tags[i].std_key { + Some(StandardTagKey::ReplayGainAlbumGain) => data.album_gain_db = value, + Some(StandardTagKey::ReplayGainAlbumPeak) => data.album_peak = value, + Some(StandardTagKey::ReplayGainTrackGain) => data.track_gain_db = value, + Some(StandardTagKey::ReplayGainTrackPeak) => data.track_peak = value, + _ => (), + } + } + i += 1; + } + + break Some(data); + } + } + } } impl AudioDecoder for SymphoniaDecoder { - fn seek(&mut self, absgp: u64) -> DecoderResult<()> { + // TODO : change to position ms + fn seek(&mut self, absgp: u64) -> Result { let seeked_to = self.format.seek( SeekMode::Accurate, - SeekTo::Time { - time: absgp, // TODO : move to Duration - track_id: Some(self.track_id), + SeekTo::TimeStamp { + ts: absgp, // TODO : move to Duration + track_id: self.track_id, }, )?; - self.position = seeked_to.actual_ts; - // TODO : Ok(self.position) - Ok(()) + Ok(seeked_to.actual_ts) } 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())), + Err(Error::IoError(err)) => { + if err.kind() == io::ErrorKind::UnexpectedEof { + return Ok(None); + } else { + return Err(DecoderError::SymphoniaDecoder(err.to_string())); + } + } + Err(err) => { + return Err(err.into()); } }; + match self.decoder.decode(&packet) { Ok(audio_buf) => { - self.position += packet.frames() as TimeStamp; - Ok(Some(packet)) + // TODO : track current playback position + self.sample_buffer.copy_interleaved_ref(audio_buf); + Ok(Some(AudioPacket::Samples( + self.sample_buffer.samples().to_vec(), + ))) } - // TODO: Handle non-fatal decoding errors and retry. - Err(e) => - return Err(DecoderError::SymphoniaDecoder(e.to_string())), + Err(Error::ResetRequired) => { + // This may happen after a seek. + self.decoder.reset(); + self.next_packet() + } + Err(err) => Err(err.into()), } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 211e1795..f9120b83 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -16,6 +16,7 @@ use std::{ use byteorder::{LittleEndian, ReadBytesExt}; use futures_util::{future, stream::futures_unordered::FuturesUnordered, StreamExt, TryFutureExt}; use parking_lot::Mutex; +use symphonia::core::io::MediaSource; use tokio::sync::{mpsc, oneshot}; use crate::{ @@ -28,7 +29,7 @@ use crate::{ config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, convert::Converter, core::{util::SeqGenerator, Error, Session, SpotifyId}, - decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder}, + decoder::{AudioDecoder, AudioPacket, PassthroughDecoder, SymphoniaDecoder}, metadata::audio::{AudioFileFormat, AudioItem}, mixer::AudioFilter, }; @@ -220,10 +221,12 @@ pub fn ratio_to_db(ratio: f64) -> f64 { #[derive(Clone, Copy, Debug)] pub struct NormalisationData { - track_gain_db: f32, - track_peak: f32, - album_gain_db: f32, - album_peak: f32, + // Spotify provides these as `f32`, but audio metadata can contain up to `f64`. + // Also, this negates the need for casting during sample processing. + pub track_gain_db: f64, + pub track_peak: f64, + pub album_gain_db: f64, + pub album_peak: f64, } impl Default for NormalisationData { @@ -238,7 +241,7 @@ impl Default for NormalisationData { } impl NormalisationData { - fn parse_from_file(mut file: T) -> io::Result { + fn parse_from_ogg(mut file: T) -> io::Result { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; let newpos = file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?; @@ -251,10 +254,10 @@ impl NormalisationData { return Ok(NormalisationData::default()); } - let track_gain_db = file.read_f32::()?; - let track_peak = file.read_f32::()?; - let album_gain_db = file.read_f32::()?; - let album_peak = file.read_f32::()?; + let track_gain_db = file.read_f32::()? as f64; + let track_peak = file.read_f32::()? as f64; + let album_gain_db = file.read_f32::()? as f64; + let album_peak = file.read_f32::()? as f64; let r = NormalisationData { track_gain_db, @@ -277,11 +280,11 @@ impl NormalisationData { [data.track_gain_db, data.track_peak] }; - let normalisation_power = gain_db as f64 + config.normalisation_pregain; + let normalisation_power = gain_db + config.normalisation_pregain; let mut normalisation_factor = db_to_ratio(normalisation_power); - if normalisation_factor * gain_peak as f64 > config.normalisation_threshold { - let limited_normalisation_factor = config.normalisation_threshold / gain_peak as f64; + if normalisation_factor * gain_peak > config.normalisation_threshold { + let limited_normalisation_factor = config.normalisation_threshold / gain_peak; let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); if config.normalisation_method == NormalisationMethod::Basic { @@ -304,7 +307,7 @@ impl NormalisationData { normalisation_factor * 100.0 ); - normalisation_factor as f64 + normalisation_factor } } @@ -820,23 +823,34 @@ impl PlayerTrackLoader { } let duration_ms = audio.duration as u32; - // (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 + // (Most) podcasts seem to support only 96 kbps Ogg Vorbis, so fall back to it let formats = match self.config.bitrate { Bitrate::Bitrate96 => [ AudioFileFormat::OGG_VORBIS_96, + AudioFileFormat::MP3_96, AudioFileFormat::OGG_VORBIS_160, + AudioFileFormat::MP3_160, + AudioFileFormat::MP3_256, AudioFileFormat::OGG_VORBIS_320, + AudioFileFormat::MP3_320, ], Bitrate::Bitrate160 => [ AudioFileFormat::OGG_VORBIS_160, + AudioFileFormat::MP3_160, AudioFileFormat::OGG_VORBIS_96, + AudioFileFormat::MP3_96, + AudioFileFormat::MP3_256, AudioFileFormat::OGG_VORBIS_320, + AudioFileFormat::MP3_320, ], Bitrate::Bitrate320 => [ AudioFileFormat::OGG_VORBIS_320, + AudioFileFormat::MP3_320, + AudioFileFormat::MP3_256, AudioFileFormat::OGG_VORBIS_160, + AudioFileFormat::MP3_160, AudioFileFormat::OGG_VORBIS_96, + AudioFileFormat::MP3_96, ], }; @@ -879,43 +893,48 @@ impl PlayerTrackLoader { let is_cached = encrypted_file.is_cached(); + // Setting up demuxing and decoding will trigger a seek() so always start in random access mode. let stream_loader_controller = encrypted_file.get_stream_loader_controller().ok()?; - - let key = match self.session.audio_key().request(spotify_id, file_id).await { - Ok(key) => key, - Err(e) => { - error!("Unable to load decryption key: {:?}", e); - return None; - } - }; - - let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); - - // Parsing normalisation data will trigger a seek() so always start in random access mode. stream_loader_controller.set_random_access_mode(); - let normalisation_data = match NormalisationData::parse_from_file(&mut decrypted_file) { - Ok(data) => data, - Err(_) => { - warn!("Unable to extract normalisation data, using default values."); - NormalisationData::default() + // Not all audio files are encrypted. If we can't get a key, try loading the track + // without decryption. If the file was encrypted after all, the decoder will fail + // parsing and bail out, so we should be safe from outputting ear-piercing noise. + let key = match self.session.audio_key().request(spotify_id, file_id).await { + Ok(key) => Some(key), + Err(e) => { + warn!("Unable to load key, continuing without decryption: {}", e); + None } }; + let decrypted_file = AudioDecrypt::new(key, encrypted_file); + let mut audio_file = + Subfile::new(decrypted_file, stream_loader_controller.len() as u64); - let audio_file = Subfile::new(decrypted_file, 0xa7); - + let mut normalisation_data = None; let result = if self.config.passthrough { - match PassthroughDecoder::new(audio_file) { - Ok(result) => Ok(Box::new(result) as Decoder), - Err(e) => Err(DecoderError::PassthroughDecoder(e.to_string())), - } + PassthroughDecoder::new(audio_file, format).map(|x| Box::new(x) as Decoder) } else { - match VorbisDecoder::new(audio_file) { - Ok(result) => Ok(Box::new(result) as Decoder), - Err(e) => Err(DecoderError::LewtonDecoder(e.to_string())), + // Spotify stores normalisation data in a custom Ogg packet instead of Vorbis comments. + if SymphoniaDecoder::is_ogg_vorbis(format) { + normalisation_data = NormalisationData::parse_from_ogg(&mut audio_file).ok(); } + + SymphoniaDecoder::new(audio_file, format).map(|mut decoder| { + // For formats other that Vorbis, we'll try getting normalisation data from + // ReplayGain metadata fields, if present. + if normalisation_data.is_none() { + normalisation_data = decoder.normalisation_data(); + } + Box::new(decoder) as Decoder + }) }; + let normalisation_data = normalisation_data.unwrap_or_else(|| { + warn!("Unable to get normalisation data, continuing with defaults."); + NormalisationData::default() + }); + let mut decoder = match result { Ok(decoder) => decoder, Err(e) if is_cached => { @@ -1035,7 +1054,7 @@ impl Future for PlayerInternal { track_id, e ); debug_assert!(self.state.is_loading()); - self.send_event(PlayerEvent::EndOfTrack { + self.send_event(PlayerEvent::Unavailable { track_id, play_request_id, }) @@ -2184,27 +2203,24 @@ impl fmt::Debug for PlayerState { } } } + struct Subfile { stream: T, - offset: u64, + length: u64, } impl Subfile { - pub fn new(mut stream: T, offset: u64) -> Subfile { - let target = SeekFrom::Start(offset); - match stream.seek(target) { + pub fn new(mut stream: T, length: u64) -> Subfile { + match stream.seek(SeekFrom::Start(0)) { Ok(pos) => { - if pos != offset { - error!( - "Subfile::new seeking to {:?} but position is now {:?}", - target, pos - ); + if pos != 0 { + error!("Subfile::new seeking to 0 but position is now {:?}", pos); } } Err(e) => error!("Subfile new Error: {}", e), } - Subfile { stream, offset } + Subfile { stream, length } } } @@ -2215,21 +2231,20 @@ impl Read for Subfile { } impl Seek for Subfile { - fn seek(&mut self, mut pos: SeekFrom) -> io::Result { - pos = match pos { - SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset), - x => x, - }; - - let newpos = self.stream.seek(pos)?; - - if newpos >= self.offset { - Ok(newpos - self.offset) - } else { - Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "newpos < self.offset", - )) - } + fn seek(&mut self, pos: SeekFrom) -> io::Result { + self.stream.seek(pos) + } +} + +impl MediaSource for Subfile +where + R: Read + Seek + Send, +{ + fn is_seekable(&self) -> bool { + true + } + + fn byte_len(&self) -> Option { + Some(self.length) } } From 60d78b6b585bcad0399072275585c486c433ac82 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 3 Jan 2022 00:44:41 +0100 Subject: [PATCH 071/147] Bump MSRV to 1.51 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aeb422cb..62931b86 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.48 # MSRV (Minimum supported rust version) + - 1.51 # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta From 84e3fe5558eecd3f427621f343ce69d4740dcd8a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 3 Jan 2022 01:01:29 +0100 Subject: [PATCH 072/147] Bump MSRV to 1.53 --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62931b86..9535537a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.51 # MSRV (Minimum supported rust version) + - 1.53 # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -113,7 +113,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - 1.48 # MSRV (Minimum supported rust version) + - 1.53 # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -160,7 +160,7 @@ jobs: os: [ubuntu-latest] target: [armv7-unknown-linux-gnueabihf] toolchain: - - 1.48 # MSRV (Minimum supported rust version) + - 1.53 # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code From 096269c1d0c528abc80f13d9bad7df10314cbc86 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 3 Jan 2022 22:20:29 +0100 Subject: [PATCH 073/147] Reintroduce offset for passthrough decoder --- metadata/src/audio/file.rs | 21 +++++++ metadata/src/audio/mod.rs | 2 +- playback/src/decoder/mod.rs | 27 --------- playback/src/decoder/passthrough_decoder.rs | 4 +- playback/src/decoder/symphonia_decoder.rs | 9 ++- playback/src/player.rs | 67 ++++++++++++++++----- 6 files changed, 81 insertions(+), 49 deletions(-) diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs index 65608814..7e33e55b 100644 --- a/metadata/src/audio/file.rs +++ b/metadata/src/audio/file.rs @@ -16,6 +16,27 @@ impl Deref for AudioFiles { } } +impl AudioFiles { + pub fn is_ogg_vorbis(format: AudioFileFormat) -> bool { + matches!( + format, + AudioFileFormat::OGG_VORBIS_320 + | AudioFileFormat::OGG_VORBIS_160 + | AudioFileFormat::OGG_VORBIS_96 + ) + } + + pub fn is_mp3(format: AudioFileFormat) -> bool { + matches!( + format, + AudioFileFormat::MP3_320 + | AudioFileFormat::MP3_256 + | AudioFileFormat::MP3_160 + | AudioFileFormat::MP3_96 + ) + } +} + impl From<&[AudioFileMessage]> for AudioFiles { fn from(files: &[AudioFileMessage]) -> Self { let audio_files = files diff --git a/metadata/src/audio/mod.rs b/metadata/src/audio/mod.rs index cc4efef0..7e31f190 100644 --- a/metadata/src/audio/mod.rs +++ b/metadata/src/audio/mod.rs @@ -1,5 +1,5 @@ pub mod file; pub mod item; -pub use file::AudioFileFormat; +pub use file::{AudioFileFormat, AudioFiles}; pub use item::AudioItem; diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index c0ede5a0..0e910846 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -1,7 +1,5 @@ use thiserror::Error; -use crate::metadata::audio::AudioFileFormat; - mod passthrough_decoder; pub use passthrough_decoder::PassthroughDecoder; @@ -59,31 +57,6 @@ impl AudioPacket { pub trait AudioDecoder { fn seek(&mut self, absgp: u64) -> Result; fn next_packet(&mut self) -> DecoderResult>; - - fn is_ogg_vorbis(format: AudioFileFormat) -> bool - where - Self: Sized, - { - matches!( - format, - AudioFileFormat::OGG_VORBIS_320 - | AudioFileFormat::OGG_VORBIS_160 - | AudioFileFormat::OGG_VORBIS_96 - ) - } - - fn is_mp3(format: AudioFileFormat) -> bool - where - Self: Sized, - { - matches!( - format, - AudioFileFormat::MP3_320 - | AudioFileFormat::MP3_256 - | AudioFileFormat::MP3_160 - | AudioFileFormat::MP3_96 - ) - } } impl From for DecoderError { diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index 9b8eedf8..bcb2591d 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -9,7 +9,7 @@ use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; -use crate::metadata::audio::AudioFileFormat; +use crate::metadata::audio::{AudioFileFormat, AudioFiles}; fn get_header(code: u8, rdr: &mut PacketReader) -> DecoderResult> where @@ -44,7 +44,7 @@ pub struct PassthroughDecoder { impl PassthroughDecoder { /// Constructs a new Decoder from a given implementation of `Read + Seek`. pub fn new(rdr: R, format: AudioFileFormat) -> DecoderResult { - if !Self::is_ogg_vorbis(format) { + if !AudioFiles::is_ogg_vorbis(format) { return Err(DecoderError::PassthroughDecoder(format!( "Passthrough decoder is not implemented for format {:?}", format diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 5546faa5..9b5c7d1e 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -12,7 +12,10 @@ use symphonia::core::{ use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; -use crate::{metadata::audio::AudioFileFormat, player::NormalisationData}; +use crate::{ + metadata::audio::{AudioFileFormat, AudioFiles}, + player::NormalisationData, +}; pub struct SymphoniaDecoder { track_id: u32, @@ -33,10 +36,10 @@ impl SymphoniaDecoder { // Not necessary, but speeds up loading. let mut hint = Hint::new(); - if Self::is_ogg_vorbis(format) { + if AudioFiles::is_ogg_vorbis(format) { hint.with_extension("ogg"); hint.mime_type("audio/ogg"); - } else if Self::is_mp3(format) { + } else if AudioFiles::is_mp3(format) { hint.with_extension("mp3"); hint.mime_type("audio/mp3"); } diff --git a/playback/src/player.rs b/playback/src/player.rs index f9120b83..43284097 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -30,7 +30,7 @@ use crate::{ convert::Converter, core::{util::SeqGenerator, Error, Session, SpotifyId}, decoder::{AudioDecoder, AudioPacket, PassthroughDecoder, SymphoniaDecoder}, - metadata::audio::{AudioFileFormat, AudioItem}, + metadata::audio::{AudioFileFormat, AudioFiles, AudioItem}, mixer::AudioFilter, }; @@ -39,6 +39,10 @@ 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; +// Spotify inserts a custom Ogg packet at the start with custom metadata values, that you would +// otherwise expect in Vorbis comments. This packet isn't well-formed and players may balk at it. +const SPOTIFY_OGG_HEADER_END: u64 = 0xa7; + pub type PlayerResult = Result<(), Error>; pub struct Player { @@ -907,19 +911,27 @@ impl PlayerTrackLoader { None } }; - let decrypted_file = AudioDecrypt::new(key, encrypted_file); - let mut audio_file = - Subfile::new(decrypted_file, stream_loader_controller.len() as u64); + let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); + + let is_ogg_vorbis = AudioFiles::is_ogg_vorbis(format); + let (offset, mut normalisation_data) = if is_ogg_vorbis { + // Spotify stores normalisation data in a custom Ogg packet instead of Vorbis comments. + let normalisation_data = + NormalisationData::parse_from_ogg(&mut decrypted_file).ok(); + (SPOTIFY_OGG_HEADER_END, normalisation_data) + } else { + (0, None) + }; + + let audio_file = Subfile::new( + decrypted_file, + offset, + stream_loader_controller.len() as u64, + ); - let mut normalisation_data = None; let result = if self.config.passthrough { PassthroughDecoder::new(audio_file, format).map(|x| Box::new(x) as Decoder) } else { - // Spotify stores normalisation data in a custom Ogg packet instead of Vorbis comments. - if SymphoniaDecoder::is_ogg_vorbis(format) { - normalisation_data = NormalisationData::parse_from_ogg(&mut audio_file).ok(); - } - SymphoniaDecoder::new(audio_file, format).map(|mut decoder| { // For formats other that Vorbis, we'll try getting normalisation data from // ReplayGain metadata fields, if present. @@ -2206,21 +2218,30 @@ impl fmt::Debug for PlayerState { struct Subfile { stream: T, + offset: u64, length: u64, } impl Subfile { - pub fn new(mut stream: T, length: u64) -> Subfile { - match stream.seek(SeekFrom::Start(0)) { + pub fn new(mut stream: T, offset: u64, length: u64) -> Subfile { + let target = SeekFrom::Start(offset); + match stream.seek(target) { Ok(pos) => { - if pos != 0 { - error!("Subfile::new seeking to 0 but position is now {:?}", pos); + if pos != offset { + error!( + "Subfile::new seeking to {:?} but position is now {:?}", + target, pos + ); } } Err(e) => error!("Subfile new Error: {}", e), } - Subfile { stream, length } + Subfile { + stream, + offset, + length, + } } } @@ -2232,7 +2253,21 @@ impl Read for Subfile { impl Seek for Subfile { fn seek(&mut self, pos: SeekFrom) -> io::Result { - self.stream.seek(pos) + let pos = match pos { + SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset), + x => x, + }; + + let newpos = self.stream.seek(pos)?; + + if newpos >= self.offset { + Ok(newpos - self.offset) + } else { + Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "newpos < self.offset", + )) + } } } From f3a66d4b991058e69fe8a8ffa3c0ef114343629b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 3 Jan 2022 22:41:54 +0100 Subject: [PATCH 074/147] Remove lewton decoder --- playback/src/decoder/lewton_decoder.rs | 46 -------------------------- 1 file changed, 46 deletions(-) delete mode 100644 playback/src/decoder/lewton_decoder.rs diff --git a/playback/src/decoder/lewton_decoder.rs b/playback/src/decoder/lewton_decoder.rs deleted file mode 100644 index 9e79c1e3..00000000 --- a/playback/src/decoder/lewton_decoder.rs +++ /dev/null @@ -1,46 +0,0 @@ -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::io::{Read, Seek}; - -pub struct VorbisDecoder(OggStreamReader); - -impl VorbisDecoder -where - R: Read + Seek, -{ - pub fn new(input: R) -> DecoderResult> { - let reader = - OggStreamReader::new(input).map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?; - Ok(VorbisDecoder(reader)) - } -} - -impl AudioDecoder for VorbisDecoder -where - R: Read + Seek, -{ - fn seek(&mut self, absgp: u64) -> Result { - self.0 - .seek_absgp_pg(absgp) - .map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?; - Ok(absgp) - } - - 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(e) => return Err(DecoderError::LewtonDecoder(e.to_string())), - } - } - } -} From 01448ccbe8d100eaf293c711f7ebc0467fcd25b0 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 4 Jan 2022 00:17:30 +0100 Subject: [PATCH 075/147] Seek in milliseconds and report the actual new position --- playback/src/decoder/mod.rs | 4 +- playback/src/decoder/passthrough_decoder.rs | 27 ++-- playback/src/decoder/symphonia_decoder.rs | 45 +++++-- playback/src/player.rs | 139 ++++++++------------ 4 files changed, 108 insertions(+), 107 deletions(-) diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index 0e910846..8d4cc1c8 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -55,8 +55,8 @@ impl AudioPacket { } pub trait AudioDecoder { - fn seek(&mut self, absgp: u64) -> Result; - fn next_packet(&mut self) -> DecoderResult>; + fn seek(&mut self, position_ms: u32) -> Result; + fn next_packet(&mut self) -> DecoderResult>; } impl From for DecoderError { diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index bcb2591d..80a649f2 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -9,7 +9,10 @@ use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; -use crate::metadata::audio::{AudioFileFormat, AudioFiles}; +use crate::{ + metadata::audio::{AudioFileFormat, AudioFiles}, + MS_PER_PAGE, PAGES_PER_MS, +}; fn get_header(code: u8, rdr: &mut PacketReader) -> DecoderResult> where @@ -23,7 +26,7 @@ where debug!("Vorbis header type {}", &pkt_type); if pkt_type != code { - return Err(DecoderError::PassthroughDecoder("Invalid Data".to_string())); + return Err(DecoderError::PassthroughDecoder("Invalid Data".into())); } Ok(pck.data.into_boxed_slice()) @@ -79,10 +82,16 @@ impl PassthroughDecoder { bos: false, }) } + + fn position_pcm_to_ms(position_pcm: u64) -> u32 { + (position_pcm as f64 * MS_PER_PAGE) as u32 + } } impl AudioDecoder for PassthroughDecoder { - fn seek(&mut self, absgp: u64) -> Result { + fn seek(&mut self, position_ms: u32) -> Result { + let absgp = (position_ms as f64 * PAGES_PER_MS) as u64; + // add an eos to previous stream if missing if self.bos && !self.eos { match self.rdr.read_packet() { @@ -118,18 +127,17 @@ impl AudioDecoder for PassthroughDecoder { let new_page = pck.absgp_page(); self.ofsgp_page = new_page; debug!("Seek to offset page {}", new_page); - Ok(new_page) + let new_position_ms = Self::position_pcm_to_ms(new_page); + Ok(new_position_ms) } - None => Err(DecoderError::PassthroughDecoder( - "Packet is None".to_string(), - )), + None => Err(DecoderError::PassthroughDecoder("Packet is None".into())), } } Err(e) => Err(DecoderError::PassthroughDecoder(e.to_string())), } } - fn next_packet(&mut self) -> DecoderResult> { + fn next_packet(&mut self) -> DecoderResult> { // write headers if we are (re)starting if !self.bos { self.wtr @@ -199,8 +207,9 @@ impl AudioDecoder for PassthroughDecoder { let data = self.wtr.inner_mut(); if !data.is_empty() { + let position_ms = Self::position_pcm_to_ms(pckgp_page); let ogg_data = AudioPacket::Raw(std::mem::take(data)); - return Ok(Some(ogg_data)); + return Ok(Some((position_ms, ogg_data))); } } } diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 9b5c7d1e..776c813c 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -8,6 +8,7 @@ use symphonia::core::{ io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions}, meta::{MetadataOptions, StandardTagKey, Value}, probe::Hint, + units::Time, }; use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; @@ -15,10 +16,10 @@ use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; use crate::{ metadata::audio::{AudioFileFormat, AudioFiles}, player::NormalisationData, + PAGES_PER_MS, }; pub struct SymphoniaDecoder { - track_id: u32, decoder: Box, format: Box, sample_buffer: SampleBuffer, @@ -85,7 +86,6 @@ impl SymphoniaDecoder { let sample_buffer = SampleBuffer::new(max_frames, SignalSpec { rate, channels }); Ok(Self { - track_id: track.id, decoder, format, sample_buffer, @@ -127,22 +127,40 @@ impl SymphoniaDecoder { } } } + + fn ts_to_ms(&self, ts: u64) -> u32 { + let time_base = self.decoder.codec_params().time_base; + let seeked_to_ms = match time_base { + Some(time_base) => { + let time = time_base.calc_time(ts); + (time.seconds as f64 + time.frac) * 1000. + } + // Fallback in the unexpected case that the format has no base time set. + None => (ts as f64 * PAGES_PER_MS), + }; + seeked_to_ms as u32 + } } impl AudioDecoder for SymphoniaDecoder { - // TODO : change to position ms - fn seek(&mut self, absgp: u64) -> Result { - let seeked_to = self.format.seek( + fn seek(&mut self, position_ms: u32) -> Result { + let seconds = position_ms as u64 / 1000; + let frac = (position_ms as f64 % 1000.) / 1000.; + let time = Time::new(seconds, frac); + + // `track_id: None` implies the default track ID (of the container, not of Spotify). + let seeked_to_ts = self.format.seek( SeekMode::Accurate, - SeekTo::TimeStamp { - ts: absgp, // TODO : move to Duration - track_id: self.track_id, + SeekTo::Time { + time, + track_id: None, }, )?; - Ok(seeked_to.actual_ts) + + Ok(self.ts_to_ms(seeked_to_ts.actual_ts)) } - fn next_packet(&mut self) -> DecoderResult> { + fn next_packet(&mut self) -> DecoderResult> { let packet = match self.format.next_packet() { Ok(packet) => packet, Err(Error::IoError(err)) => { @@ -159,11 +177,10 @@ impl AudioDecoder for SymphoniaDecoder { match self.decoder.decode(&packet) { Ok(audio_buf) => { - // TODO : track current playback position self.sample_buffer.copy_interleaved_ref(audio_buf); - Ok(Some(AudioPacket::Samples( - self.sample_buffer.samples().to_vec(), - ))) + let position_ms = self.ts_to_ms(packet.pts()); + let samples = AudioPacket::Samples(self.sample_buffer.samples().to_vec()); + Ok(Some((position_ms, samples))) } Err(Error::ResetRequired) => { // This may happen after a seek. diff --git a/playback/src/player.rs b/playback/src/player.rs index 43284097..129a79ff 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -34,7 +34,7 @@ use crate::{ mixer::AudioFilter, }; -use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND}; +use crate::SAMPLES_PER_SECOND; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; pub const DB_VOLTAGE_RATIO: f64 = 20.0; @@ -489,7 +489,7 @@ struct PlayerLoadedTrackData { stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, - stream_position_pcm: u64, + stream_position_ms: u32, is_explicit: bool, } @@ -524,7 +524,7 @@ enum PlayerState { stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, - stream_position_pcm: u64, + stream_position_ms: u32, suggested_to_preload_next_track: bool, is_explicit: bool, }, @@ -537,7 +537,7 @@ enum PlayerState { stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, - stream_position_pcm: u64, + stream_position_ms: u32, reported_nominal_start_time: Option, suggested_to_preload_next_track: bool, is_explicit: bool, @@ -622,7 +622,7 @@ impl PlayerState { bytes_per_second, normalisation_data, stream_loader_controller, - stream_position_pcm, + stream_position_ms, is_explicit, .. } => { @@ -635,7 +635,7 @@ impl PlayerState { stream_loader_controller, bytes_per_second, duration_ms, - stream_position_pcm, + stream_position_ms, is_explicit, }, }; @@ -663,7 +663,7 @@ impl PlayerState { stream_loader_controller, duration_ms, bytes_per_second, - stream_position_pcm, + stream_position_ms, suggested_to_preload_next_track, is_explicit, } => { @@ -676,7 +676,7 @@ impl PlayerState { stream_loader_controller, duration_ms, bytes_per_second, - stream_position_pcm, + stream_position_ms, reported_nominal_start_time: None, suggested_to_preload_next_track, is_explicit, @@ -705,7 +705,7 @@ impl PlayerState { stream_loader_controller, duration_ms, bytes_per_second, - stream_position_pcm, + stream_position_ms, reported_nominal_start_time: _, suggested_to_preload_next_track, is_explicit, @@ -719,7 +719,7 @@ impl PlayerState { stream_loader_controller, duration_ms, bytes_per_second, - stream_position_pcm, + stream_position_ms, suggested_to_preload_next_track, is_explicit, }; @@ -981,13 +981,12 @@ impl PlayerTrackLoader { // the cursor may have been moved by parsing normalisation data. This may not // matter for playback (but won't hurt either), but may be useful for the // passthrough decoder. - let position_pcm = PlayerInternal::position_ms_to_pcm(position_ms); - let stream_position_pcm = match decoder.seek(position_pcm) { - Ok(_) => position_pcm, + let stream_position_ms = match decoder.seek(position_ms) { + Ok(_) => position_ms, Err(e) => { warn!( - "PlayerTrackLoader::load_track error seeking to PCM page {}: {}", - position_pcm, e + "PlayerTrackLoader::load_track error seeking to {}: {}", + position_ms, e ); 0 } @@ -1005,7 +1004,7 @@ impl PlayerTrackLoader { stream_loader_controller, bytes_per_second, duration_ms, - stream_position_pcm, + stream_position_ms, is_explicit, }); } @@ -1118,23 +1117,18 @@ impl Future for PlayerInternal { play_request_id, ref mut decoder, normalisation_factor, - ref mut stream_position_pcm, + ref mut stream_position_ms, ref mut reported_nominal_start_time, duration_ms, .. } = self.state { match decoder.next_packet() { - Ok(packet) => { - if !passthrough { - if let Some(ref packet) = packet { + Ok(result) => { + if let Some((new_stream_position_ms, ref packet)) = result { + if !passthrough { 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); - + Ok(_) => { let notify_about_position = match *reported_nominal_start_time { None => true, @@ -1144,7 +1138,7 @@ impl Future for PlayerInternal { - reported_nominal_start_time) .as_millis() as i64 - - stream_position_millis as i64; + - new_stream_position_ms as i64; lag > Duration::from_secs(1).as_millis() as i64 } @@ -1153,13 +1147,13 @@ impl Future for PlayerInternal { *reported_nominal_start_time = Some( Instant::now() - Duration::from_millis( - stream_position_millis as u64, + new_stream_position_ms as u64, ), ); self.send_event(PlayerEvent::Playing { track_id, play_request_id, - position_ms: stream_position_millis as u32, + position_ms: new_stream_position_ms as u32, duration_ms, }); } @@ -1172,13 +1166,13 @@ impl Future for PlayerInternal { }) } } + } else { + // position, even if irrelevant, must be set so that seek() is called + *stream_position_ms = new_stream_position_ms; } - } 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(result, normalisation_factor); } Err(e) => { error!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e); @@ -1198,7 +1192,7 @@ impl Future for PlayerInternal { track_id, play_request_id, duration_ms, - stream_position_pcm, + stream_position_ms, ref mut stream_loader_controller, ref mut suggested_to_preload_next_track, .. @@ -1207,14 +1201,14 @@ impl Future for PlayerInternal { track_id, play_request_id, duration_ms, - stream_position_pcm, + stream_position_ms, ref mut stream_loader_controller, ref mut suggested_to_preload_next_track, .. } = self.state { if (!*suggested_to_preload_next_track) - && ((duration_ms as i64 - Self::position_pcm_to_ms(stream_position_pcm) as i64) + && ((duration_ms as i64 - stream_position_ms as i64) < PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64) && stream_loader_controller.range_to_end_available() { @@ -1238,14 +1232,6 @@ impl Future for PlayerInternal { } impl PlayerInternal { - fn position_pcm_to_ms(position_pcm: u64) -> u32 { - (position_pcm as f64 * MS_PER_PAGE) as u32 - } - - fn position_ms_to_pcm(position_ms: u32) -> u64 { - (position_ms as f64 * PAGES_PER_MS) as u64 - } - fn ensure_sink_running(&mut self) { if self.sink_status != SinkStatus::Running { trace!("== Starting sink =="); @@ -1336,18 +1322,16 @@ impl PlayerInternal { if let PlayerState::Paused { track_id, play_request_id, - stream_position_pcm, + stream_position_ms, duration_ms, .. } = self.state { self.state.paused_to_playing(); - - let position_ms = Self::position_pcm_to_ms(stream_position_pcm); self.send_event(PlayerEvent::Playing { track_id, play_request_id, - position_ms, + position_ms: stream_position_ms, duration_ms, }); self.ensure_sink_running(); @@ -1360,7 +1344,7 @@ impl PlayerInternal { if let PlayerState::Playing { track_id, play_request_id, - stream_position_pcm, + stream_position_ms, duration_ms, .. } = self.state @@ -1368,11 +1352,10 @@ impl PlayerInternal { self.state.playing_to_paused(); self.ensure_sink_stopped(false); - let position_ms = Self::position_pcm_to_ms(stream_position_pcm); self.send_event(PlayerEvent::Paused { track_id, play_request_id, - position_ms, + position_ms: stream_position_ms, duration_ms, }); } else { @@ -1380,9 +1363,9 @@ impl PlayerInternal { } } - fn handle_packet(&mut self, packet: Option, normalisation_factor: f64) { + fn handle_packet(&mut self, packet: Option<(u32, AudioPacket)>, normalisation_factor: f64) { match packet { - Some(mut packet) => { + Some((_, mut packet)) => { if !packet.is_empty() { if let AudioPacket::Samples(ref mut data) = packet { if self.config.normalisation @@ -1537,7 +1520,7 @@ impl PlayerInternal { loaded_track: PlayerLoadedTrackData, start_playback: bool, ) { - let position_ms = Self::position_pcm_to_ms(loaded_track.stream_position_pcm); + let position_ms = loaded_track.stream_position_ms; let mut config = self.config.clone(); if config.normalisation_type == NormalisationType::Auto { @@ -1569,7 +1552,7 @@ impl PlayerInternal { stream_loader_controller: loaded_track.stream_loader_controller, duration_ms: loaded_track.duration_ms, bytes_per_second: loaded_track.bytes_per_second, - stream_position_pcm: loaded_track.stream_position_pcm, + stream_position_ms: loaded_track.stream_position_ms, reported_nominal_start_time: Some( Instant::now() - Duration::from_millis(position_ms as u64), ), @@ -1588,7 +1571,7 @@ impl PlayerInternal { stream_loader_controller: loaded_track.stream_loader_controller, duration_ms: loaded_track.duration_ms, bytes_per_second: loaded_track.bytes_per_second, - stream_position_pcm: loaded_track.stream_position_pcm, + stream_position_ms: loaded_track.stream_position_ms, suggested_to_preload_next_track: false, is_explicit: loaded_track.is_explicit, }; @@ -1666,15 +1649,13 @@ impl PlayerInternal { } }; - let position_pcm = Self::position_ms_to_pcm(position_ms); - - if position_pcm != loaded_track.stream_position_pcm { + if position_ms != loaded_track.stream_position_ms { loaded_track .stream_loader_controller .set_random_access_mode(); // This may be blocking. - match loaded_track.decoder.seek(position_pcm) { - Ok(_) => loaded_track.stream_position_pcm = position_pcm, + match loaded_track.decoder.seek(position_ms) { + Ok(_) => loaded_track.stream_position_ms = position_ms, Err(e) => error!("PlayerInternal handle_command_load: {}", e), } loaded_track.stream_loader_controller.set_stream_mode(); @@ -1692,14 +1673,14 @@ impl PlayerInternal { // Check if we are already playing the track. If so, just do a seek and update our info. if let PlayerState::Playing { track_id: current_track_id, - ref mut stream_position_pcm, + ref mut stream_position_ms, ref mut decoder, ref mut stream_loader_controller, .. } | PlayerState::Paused { track_id: current_track_id, - ref mut stream_position_pcm, + ref mut stream_position_ms, ref mut decoder, ref mut stream_loader_controller, .. @@ -1707,13 +1688,11 @@ impl PlayerInternal { { if current_track_id == track_id { // we can use the current decoder. Ensure it's at the correct position. - let position_pcm = Self::position_ms_to_pcm(position_ms); - - if position_pcm != *stream_position_pcm { + if position_ms != *stream_position_ms { stream_loader_controller.set_random_access_mode(); // This may be blocking. - match decoder.seek(position_pcm) { - Ok(_) => *stream_position_pcm = position_pcm, + match decoder.seek(position_ms) { + Ok(_) => *stream_position_ms = position_ms, Err(e) => { error!("PlayerInternal::handle_command_load error seeking: {}", e) } @@ -1726,7 +1705,7 @@ impl PlayerInternal { let old_state = mem::replace(&mut self.state, PlayerState::Invalid); if let PlayerState::Playing { - stream_position_pcm, + stream_position_ms, decoder, stream_loader_controller, bytes_per_second, @@ -1736,7 +1715,7 @@ impl PlayerInternal { .. } | PlayerState::Paused { - stream_position_pcm, + stream_position_ms, decoder, stream_loader_controller, bytes_per_second, @@ -1752,7 +1731,7 @@ impl PlayerInternal { stream_loader_controller, bytes_per_second, duration_ms, - stream_position_pcm, + stream_position_ms, is_explicit, }; @@ -1785,15 +1764,13 @@ impl PlayerInternal { mut loaded_track, } = preload { - let position_pcm = Self::position_ms_to_pcm(position_ms); - - if position_pcm != loaded_track.stream_position_pcm { + if position_ms != loaded_track.stream_position_ms { loaded_track .stream_loader_controller .set_random_access_mode(); // This may be blocking - match loaded_track.decoder.seek(position_pcm) { - Ok(_) => loaded_track.stream_position_pcm = position_pcm, + match loaded_track.decoder.seek(position_ms) { + Ok(_) => loaded_track.stream_position_ms = position_ms, Err(e) => error!("PlayerInternal handle_command_load: {}", e), } loaded_track.stream_loader_controller.set_stream_mode(); @@ -1908,20 +1885,18 @@ impl PlayerInternal { stream_loader_controller.set_random_access_mode(); } if let Some(decoder) = self.state.decoder() { - let position_pcm = Self::position_ms_to_pcm(position_ms); - - match decoder.seek(position_pcm) { + match decoder.seek(position_ms) { Ok(_) => { if let PlayerState::Playing { - ref mut stream_position_pcm, + ref mut stream_position_ms, .. } | PlayerState::Paused { - ref mut stream_position_pcm, + ref mut stream_position_ms, .. } = self.state { - *stream_position_pcm = position_pcm; + *stream_position_ms = position_ms; } } Err(e) => error!("PlayerInternal::handle_command_seek error: {}", e), From d5a4be4aa18c0761b2453d1deb471f61679846d0 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 4 Jan 2022 00:50:45 +0100 Subject: [PATCH 076/147] Optimize fallback sample buffer size --- metadata/src/audio/file.rs | 5 +++++ playback/src/decoder/symphonia_decoder.rs | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs index 7e33e55b..237b8e31 100644 --- a/metadata/src/audio/file.rs +++ b/metadata/src/audio/file.rs @@ -33,8 +33,13 @@ impl AudioFiles { | AudioFileFormat::MP3_256 | AudioFileFormat::MP3_160 | AudioFileFormat::MP3_96 + | AudioFileFormat::MP3_160_ENC ) } + + pub fn is_flac(format: AudioFileFormat) -> bool { + matches!(format, AudioFileFormat::FLAC_FLAC) + } } impl From<&[AudioFileMessage]> for AudioFiles { diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 776c813c..dcf6950d 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -43,8 +43,20 @@ impl SymphoniaDecoder { } else if AudioFiles::is_mp3(format) { hint.with_extension("mp3"); hint.mime_type("audio/mp3"); + } else if AudioFiles::is_flac(format) { + hint.with_extension("flac"); + hint.mime_type("audio/flac"); } + let max_format_size = if AudioFiles::is_ogg_vorbis(format) { + 8192 + } else if AudioFiles::is_mp3(format) { + 2304 + } else { + // like FLAC + 65535 + }; + let format_opts = Default::default(); let metadata_opts: MetadataOptions = Default::default(); let decoder_opts: DecoderOptions = Default::default(); @@ -81,8 +93,10 @@ impl SymphoniaDecoder { ))); } - // TODO: settle on a sane default depending on the format - let max_frames = decoder.codec_params().max_frames_per_packet.unwrap_or(8192); + let max_frames = decoder + .codec_params() + .max_frames_per_packet + .unwrap_or(max_format_size); let sample_buffer = SampleBuffer::new(max_frames, SignalSpec { rate, channels }); Ok(Self { From a49bcb70a7c0827fca5edc6bbcc63cf48794c97b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 4 Jan 2022 21:22:52 +0100 Subject: [PATCH 077/147] Fix clippy lints --- playback/src/audio_backend/portaudio.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 12a5404d..1681ad07 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -153,15 +153,15 @@ impl<'a> Sink for PortAudioSink<'a> { let result = match self { Self::F32(stream, _parameters) => { - let samples_f32: &[f32] = &converter.f64_to_f32(&samples); + let samples_f32: &[f32] = &converter.f64_to_f32(samples); write_sink!(ref mut stream, samples_f32) } Self::S32(stream, _parameters) => { - let samples_s32: &[i32] = &converter.f64_to_s32(&samples); + let samples_s32: &[i32] = &converter.f64_to_s32(samples); write_sink!(ref mut stream, samples_s32) } Self::S16(stream, _parameters) => { - let samples_s16: &[i16] = &converter.f64_to_s16(&samples); + let samples_s16: &[i16] = &converter.f64_to_s16(samples); write_sink!(ref mut stream, samples_s16) } }; From eabdd7927551d10329d08cfbe944b62d130d2d67 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 4 Jan 2022 21:23:53 +0100 Subject: [PATCH 078/147] Seeking, buffer size and error handing improvements - Set ideal sample buffer size after decoding first full packet - Prevent audio glitches after seeking - Reset decoder when the format reader requires it Credits: @pdeljanov --- playback/src/decoder/symphonia_decoder.rs | 62 ++++++++++++----------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index dcf6950d..eba819ee 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -1,7 +1,7 @@ use std::io; use symphonia::core::{ - audio::{SampleBuffer, SignalSpec}, + audio::SampleBuffer, codecs::{Decoder, DecoderOptions}, errors::Error, formats::{FormatReader, SeekMode, SeekTo}, @@ -16,13 +16,13 @@ use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; use crate::{ metadata::audio::{AudioFileFormat, AudioFiles}, player::NormalisationData, - PAGES_PER_MS, + NUM_CHANNELS, PAGES_PER_MS, SAMPLE_RATE, }; pub struct SymphoniaDecoder { decoder: Box, format: Box, - sample_buffer: SampleBuffer, + sample_buffer: Option>, } impl SymphoniaDecoder { @@ -48,15 +48,6 @@ impl SymphoniaDecoder { hint.mime_type("audio/flac"); } - let max_format_size = if AudioFiles::is_ogg_vorbis(format) { - 8192 - } else if AudioFiles::is_mp3(format) { - 2304 - } else { - // like FLAC - 65535 - }; - let format_opts = Default::default(); let metadata_opts: MetadataOptions = Default::default(); let decoder_opts: DecoderOptions = Default::default(); @@ -79,30 +70,27 @@ impl SymphoniaDecoder { DecoderError::SymphoniaDecoder("Could not retrieve channel configuration".into()) })?; - if rate != crate::SAMPLE_RATE { + if rate != SAMPLE_RATE { return Err(DecoderError::SymphoniaDecoder(format!( "Unsupported sample rate: {}", rate ))); } - if channels.count() != crate::NUM_CHANNELS as usize { + if channels.count() != NUM_CHANNELS as usize { return Err(DecoderError::SymphoniaDecoder(format!( "Unsupported number of channels: {}", channels ))); } - let max_frames = decoder - .codec_params() - .max_frames_per_packet - .unwrap_or(max_format_size); - let sample_buffer = SampleBuffer::new(max_frames, SignalSpec { rate, channels }); - Ok(Self { decoder, format, - sample_buffer, + + // We set the sample buffer when decoding the first full packet, + // whose duration is also the ideal sample buffer size. + sample_buffer: None, }) } @@ -171,6 +159,10 @@ impl AudioDecoder for SymphoniaDecoder { }, )?; + // Seeking is a `FormatReader` operation, so the decoder cannot reliably + // know when a seek took place. Reset it to avoid audio glitches. + self.decoder.reset(); + Ok(self.ts_to_ms(seeked_to_ts.actual_ts)) } @@ -184,23 +176,33 @@ impl AudioDecoder for SymphoniaDecoder { return Err(DecoderError::SymphoniaDecoder(err.to_string())); } } + Err(Error::ResetRequired) => { + self.decoder.reset(); + return self.next_packet(); + } Err(err) => { return Err(err.into()); } }; + let position_ms = self.ts_to_ms(packet.pts()); + match self.decoder.decode(&packet) { - Ok(audio_buf) => { - self.sample_buffer.copy_interleaved_ref(audio_buf); - let position_ms = self.ts_to_ms(packet.pts()); - let samples = AudioPacket::Samples(self.sample_buffer.samples().to_vec()); + Ok(decoded) => { + if self.sample_buffer.is_none() { + let spec = *decoded.spec(); + let duration = decoded.capacity() as u64; + self.sample_buffer + .replace(SampleBuffer::new(duration, spec)); + } + + let sample_buffer = self.sample_buffer.as_mut().unwrap(); // guaranteed above + sample_buffer.copy_interleaved_ref(decoded); + let samples = AudioPacket::Samples(sample_buffer.samples().to_vec()); Ok(Some((position_ms, samples))) } - Err(Error::ResetRequired) => { - // This may happen after a seek. - self.decoder.reset(); - self.next_packet() - } + // Also propagate `ResetRequired` errors from the decoder to the player, + // so that it will skip to the next track and reload the entire Symphonia decoder. Err(err) => Err(err.into()), } } From 3e09eff906a3a9af35c564d8d017ec87f5233ddd Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 4 Jan 2022 22:57:00 +0100 Subject: [PATCH 079/147] Improve initial loading time - Configure the decoder according to Spotify's metadata, don't probe - Return from `AudioFile::open` as soon as possible, with the smallest possible block size suitable for opening the decoder, so the UI transitions from loading to playing/paused state. From there, the regular prefetching will take over. --- audio/src/fetch/mod.rs | 53 +++++---------- playback/src/decoder/symphonia_decoder.rs | 80 ++++++++++++----------- playback/src/player.rs | 8 +-- 3 files changed, 60 insertions(+), 81 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index f3229574..9185e14e 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -65,10 +65,7 @@ pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128; /// 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. -pub const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 128; - -/// The ping time that is used for calculations before a ping time was actually measured. -pub const INITIAL_PING_TIME_ESTIMATE: Duration = Duration::from_millis(500); +pub const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 8; /// 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. @@ -321,7 +318,6 @@ impl AudioFile { session: &Session, file_id: FileId, bytes_per_second: usize, - play_from_beginning: bool, ) -> Result { if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) { debug!("File {} already in cache", file_id); @@ -332,13 +328,8 @@ impl AudioFile { let (complete_tx, complete_rx) = oneshot::channel(); - let streaming = AudioFileStreaming::open( - session.clone(), - file_id, - complete_tx, - bytes_per_second, - play_from_beginning, - ); + let streaming = + AudioFileStreaming::open(session.clone(), file_id, complete_tx, bytes_per_second); let session_ = session.clone(); session.spawn(complete_rx.map_ok(move |mut file| { @@ -386,38 +377,26 @@ impl AudioFileStreaming { file_id: FileId, complete_tx: oneshot::Sender, bytes_per_second: usize, - play_from_beginning: bool, ) -> Result { - // When the audio file is really small, this `download_size` may turn out to be - // larger than the audio file we're going to stream later on. This is OK; requesting - // `Content-Range` > `Content-Length` will return the complete file with status code - // 206 Partial Content. - 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 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)?; - let request_time = Instant::now(); + // When the audio file is really small, this `download_size` may turn out to be + // larger than the audio file we're going to stream later on. This is OK; requesting + // `Content-Range` > `Content-Length` will return the complete file with status code + // 206 Partial Content. + let mut streamer = + session + .spclient() + .stream_from_cdn(&cdn_url, 0, INITIAL_DOWNLOAD_SIZE)?; // 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 request_time = Instant::now(); let response = streamer.next().await.ok_or(AudioFileError::NoData)??; let header_value = response @@ -425,14 +404,16 @@ impl AudioFileStreaming { .get(CONTENT_RANGE) .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 hyphen_index = str_value.find('-').unwrap_or_default(); + let slash_index = str_value.find('/').unwrap_or_default(); + let upper_bound: usize = str_value[hyphen_index + 1..slash_index].parse()?; + let file_size = str_value[slash_index + 1..].parse()?; let initial_request = StreamingRequest { streamer, initial_response: Some(response), offset: 0, - length: download_size, + length: upper_bound + 1, request_time, }; diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index eba819ee..3b585007 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -1,14 +1,19 @@ use std::io; -use symphonia::core::{ - audio::SampleBuffer, - codecs::{Decoder, DecoderOptions}, - errors::Error, - formats::{FormatReader, SeekMode, SeekTo}, - io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions}, - meta::{MetadataOptions, StandardTagKey, Value}, - probe::Hint, - units::Time, +use symphonia::{ + core::{ + audio::SampleBuffer, + codecs::{Decoder, DecoderOptions}, + errors::Error, + formats::{FormatReader, SeekMode, SeekTo}, + io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions}, + meta::{StandardTagKey, Value}, + units::Time, + }, + default::{ + codecs::{Mp3Decoder, VorbisDecoder}, + formats::{Mp3Reader, OggReader}, + }, }; use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; @@ -20,13 +25,13 @@ use crate::{ }; pub struct SymphoniaDecoder { - decoder: Box, format: Box, + decoder: Box, sample_buffer: Option>, } impl SymphoniaDecoder { - pub fn new(input: R, format: AudioFileFormat) -> DecoderResult + pub fn new(input: R, file_format: AudioFileFormat) -> DecoderResult where R: MediaSource + 'static, { @@ -35,41 +40,37 @@ impl SymphoniaDecoder { }; let mss = MediaSourceStream::new(Box::new(input), mss_opts); - // Not necessary, but speeds up loading. - let mut hint = Hint::new(); - if AudioFiles::is_ogg_vorbis(format) { - hint.with_extension("ogg"); - hint.mime_type("audio/ogg"); - } else if AudioFiles::is_mp3(format) { - hint.with_extension("mp3"); - hint.mime_type("audio/mp3"); - } else if AudioFiles::is_flac(format) { - hint.with_extension("flac"); - hint.mime_type("audio/flac"); - } - let format_opts = Default::default(); - let metadata_opts: MetadataOptions = Default::default(); - let decoder_opts: DecoderOptions = Default::default(); - - let probed = - symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts)?; - let format = probed.format; + let format: Box = if AudioFiles::is_ogg_vorbis(file_format) { + Box::new(OggReader::try_new(mss, &format_opts)?) + } else if AudioFiles::is_mp3(file_format) { + Box::new(Mp3Reader::try_new(mss, &format_opts)?) + } else { + return Err(DecoderError::SymphoniaDecoder(format!( + "Unsupported format: {:?}", + file_format + ))); + }; let track = format.default_track().ok_or_else(|| { DecoderError::SymphoniaDecoder("Could not retrieve default track".into()) })?; - let decoder = symphonia::default::get_codecs().make(&track.codec_params, &decoder_opts)?; + let decoder_opts: DecoderOptions = Default::default(); + let decoder: Box = if AudioFiles::is_ogg_vorbis(file_format) { + Box::new(VorbisDecoder::try_new(&track.codec_params, &decoder_opts)?) + } else if AudioFiles::is_mp3(file_format) { + Box::new(Mp3Decoder::try_new(&track.codec_params, &decoder_opts)?) + } else { + return Err(DecoderError::SymphoniaDecoder(format!( + "Unsupported decoder: {:?}", + file_format + ))); + }; - let codec_params = decoder.codec_params(); - let rate = codec_params.sample_rate.ok_or_else(|| { + let rate = decoder.codec_params().sample_rate.ok_or_else(|| { DecoderError::SymphoniaDecoder("Could not retrieve sample rate".into()) })?; - let channels = codec_params.channels.ok_or_else(|| { - DecoderError::SymphoniaDecoder("Could not retrieve channel configuration".into()) - })?; - if rate != SAMPLE_RATE { return Err(DecoderError::SymphoniaDecoder(format!( "Unsupported sample rate: {}", @@ -77,6 +78,9 @@ impl SymphoniaDecoder { ))); } + let channels = decoder.codec_params().channels.ok_or_else(|| { + DecoderError::SymphoniaDecoder("Could not retrieve channel configuration".into()) + })?; if channels.count() != NUM_CHANNELS as usize { return Err(DecoderError::SymphoniaDecoder(format!( "Unsupported number of channels: {}", @@ -85,8 +89,8 @@ impl SymphoniaDecoder { } Ok(Self { - decoder, format, + decoder, // We set the sample buffer when decoding the first full packet, // whose duration is also the ideal sample buffer size. diff --git a/playback/src/player.rs b/playback/src/player.rs index 9ebd455c..d5d4b269 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -875,17 +875,11 @@ impl PlayerTrackLoader { }; let bytes_per_second = self.stream_data_rate(format); - let play_from_beginning = position_ms == 0; // This is only a loop to be able to reload the file if an error occured // while opening a cached file. loop { - let encrypted_file = AudioFile::open( - &self.session, - file_id, - bytes_per_second, - play_from_beginning, - ); + let encrypted_file = AudioFile::open(&self.session, file_id, bytes_per_second); let encrypted_file = match encrypted_file.await { Ok(encrypted_file) => encrypted_file, From 5c2b5a21c16537eb3bd9f82f7cd92ab42f970b83 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 5 Jan 2022 16:43:46 +0100 Subject: [PATCH 080/147] Fix audio file caching --- audio/src/fetch/mod.rs | 2 +- core/src/cache.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 9185e14e..5b39dc08 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -336,7 +336,7 @@ impl AudioFile { debug!("Downloading file {} complete", file_id); if let Some(cache) = session_.cache() { - if let Some(cache_id) = cache.file(file_id) { + if let Some(cache_id) = cache.file_path(file_id) { if let Err(e) = cache.save_file(file_id, &mut file) { error!("Error caching file {} to {:?}: {}", file_id, cache_id, e); } else { diff --git a/core/src/cache.rs b/core/src/cache.rs index 9484bb16..9b81e943 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -367,7 +367,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]); @@ -396,7 +396,7 @@ impl Cache { } } - pub fn save_file(&self, file: FileId, contents: &mut F) -> Result<(), Error> { + pub fn save_file(&self, file: FileId, contents: &mut F) -> Result { if let Some(path) = self.file_path(file) { if let Some(parent) = path.parent() { if let Ok(size) = fs::create_dir_all(parent) @@ -407,7 +407,7 @@ impl Cache { limiter.add(&path, size); limiter.prune()?; } - return Ok(()); + return Ok(path); } } } From 1a7c440bd729e1b3ec11576acb1cfb9109dab308 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 5 Jan 2022 20:44:08 +0100 Subject: [PATCH 081/147] Improve lock ordering and contention --- audio/src/fetch/mod.rs | 60 ++++++++++++++++++++++++++++---------- audio/src/fetch/receive.rs | 5 ++-- core/src/apresolve.rs | 27 ++++++++++------- core/src/session.rs | 4 +-- core/src/spclient.rs | 17 ++++++----- playback/src/player.rs | 17 +++++------ 6 files changed, 82 insertions(+), 48 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 5b39dc08..ad1b98e1 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -5,7 +5,7 @@ use std::{ fs, io::{self, Read, Seek, SeekFrom}, sync::{ - atomic::{self, AtomicUsize}, + atomic::{AtomicUsize, Ordering}, Arc, }, time::{Duration, Instant}, @@ -67,6 +67,9 @@ pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128; /// another position, then only this amount is requested on the first request. pub const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 8; +/// The ping time that is used for calculations before a ping time was actually measured. +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. pub const MAXIMUM_ASSUMED_PING_TIME: Duration = Duration::from_millis(1500); @@ -174,7 +177,7 @@ impl StreamLoaderController { 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); + let read_position = shared.read_position.load(Ordering::Acquire); self.range_available(Range::new(read_position, self.len() - read_position)) } None => true, @@ -183,7 +186,7 @@ impl StreamLoaderController { pub fn ping_time(&self) -> Duration { Duration::from_millis(self.stream_shared.as_ref().map_or(0, |shared| { - shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64 + shared.ping_time_ms.load(Ordering::Relaxed) as u64 })) } @@ -244,21 +247,23 @@ impl StreamLoaderController { Ok(()) } + #[allow(dead_code)] pub fn fetch_next(&self, length: usize) { if let Some(ref shared) = self.stream_shared { let range = Range { - start: shared.read_position.load(atomic::Ordering::Relaxed), + start: shared.read_position.load(Ordering::Acquire), length, }; self.fetch(range); } } + #[allow(dead_code)] 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), + start: shared.read_position.load(Ordering::Acquire), length, }; self.fetch_blocking(range) @@ -267,6 +272,31 @@ impl StreamLoaderController { } } + pub fn fetch_next_and_wait( + &self, + request_length: usize, + wait_length: usize, + ) -> AudioFileResult { + match self.stream_shared { + Some(ref shared) => { + let start = shared.read_position.load(Ordering::Acquire); + + let request_range = Range { + start, + length: request_length, + }; + self.fetch(request_range); + + let wait_range = Range { + start, + length: wait_length, + }; + self.fetch_blocking(wait_range) + } + None => Ok(()), + } + } + pub fn set_random_access_mode(&self) { // optimise download strategy for random access self.send_stream_loader_command(StreamLoaderCommand::RandomAccessMode()); @@ -428,7 +458,7 @@ impl AudioFileStreaming { }), download_strategy: Mutex::new(DownloadStrategy::Streaming()), number_of_open_requests: AtomicUsize::new(0), - ping_time_ms: AtomicUsize::new(0), + ping_time_ms: AtomicUsize::new(INITIAL_PING_TIME_ESTIMATE.as_millis() as usize), read_position: AtomicUsize::new(0), }); @@ -465,15 +495,17 @@ impl Read for AudioFileStreaming { } let length = min(output.len(), self.shared.file_size - offset); + if length == 0 { + return Ok(0); + } 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. - let ping_time_seconds = Duration::from_millis( - self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64, - ) - .as_secs_f32(); + let ping_time_seconds = + Duration::from_millis(self.shared.ping_time_ms.load(Ordering::Relaxed) as u64) + .as_secs_f32(); let length_to_request = length + max( @@ -501,10 +533,6 @@ impl Read for AudioFileStreaming { .map_err(|err| io::Error::new(io::ErrorKind::BrokenPipe, err))?; } - if length == 0 { - return Ok(0); - } - while !download_status.downloaded.contains(offset) { if self .shared @@ -531,7 +559,7 @@ impl Read for AudioFileStreaming { self.position += read_len as u64; self.shared .read_position - .store(self.position as usize, atomic::Ordering::Relaxed); + .store(self.position as usize, Ordering::Release); Ok(read_len) } @@ -543,7 +571,7 @@ impl Seek for AudioFileStreaming { // Do not seek past EOF self.shared .read_position - .store(self.position as usize, atomic::Ordering::Relaxed); + .store(self.position as usize, Ordering::Release); Ok(self.position) } } diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index b3d97eb4..08013b5b 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -1,11 +1,10 @@ use std::{ cmp::{max, min}, io::{Seek, SeekFrom, Write}, - sync::{atomic, Arc}, + sync::{atomic::Ordering, Arc}, time::{Duration, Instant}, }; -use atomic::Ordering; use bytes::Bytes; use futures_util::StreamExt; use hyper::StatusCode; @@ -231,7 +230,7 @@ impl AudioFileFetch { // download data from after the current read position first let mut tail_end = RangeSet::new(); - let read_position = self.shared.read_position.load(Ordering::Relaxed); + let read_position = self.shared.read_position.load(Ordering::Acquire); tail_end.add_range(&Range::new( read_position, self.shared.file_size - read_position, diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 69a8e15c..1e1c6de6 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,4 +1,7 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::{ + hint, + sync::atomic::{AtomicBool, Ordering}, +}; use hyper::{Body, Method, Request}; use serde::Deserialize; @@ -37,7 +40,7 @@ impl Default for ApResolveData { component! { ApResolver : ApResolverInner { data: AccessPoints = AccessPoints::default(), - spinlock: AtomicUsize = AtomicUsize::new(0), + in_progress: AtomicBool = AtomicBool::new(false), } } @@ -107,16 +110,15 @@ impl ApResolver { }) } - pub async fn resolve(&self, endpoint: &str) -> SocketAddress { + pub async fn resolve(&self, endpoint: &str) -> Result { // Use a spinlock to make this function atomic. Otherwise, various race conditions may // occur, e.g. when the session is created, multiple components are launched almost in // parallel and they will all call this function, while resolving is still in progress. self.lock(|inner| { - while inner.spinlock.load(Ordering::SeqCst) != 0 { - #[allow(deprecated)] - std::sync::atomic::spin_loop_hint() + while inner.in_progress.load(Ordering::Acquire) { + hint::spin_loop(); } - inner.spinlock.store(1, Ordering::SeqCst); + inner.in_progress.store(true, Ordering::Release); }); if self.is_empty() { @@ -131,10 +133,15 @@ impl ApResolver { "accesspoint" => inner.data.accesspoint.remove(0), "dealer" => inner.data.dealer.remove(0), "spclient" => inner.data.spclient.remove(0), - _ => unimplemented!(), + _ => { + return Err(Error::unimplemented(format!( + "No implementation to resolve access point {}", + endpoint + ))) + } }; - inner.spinlock.store(0, Ordering::SeqCst); - access_point + inner.in_progress.store(false, Ordering::Release); + Ok(access_point) }) } } diff --git a/core/src/session.rs b/core/src/session.rs index aecdaada..2b431715 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -110,7 +110,7 @@ impl Session { ) -> 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); + let session_id = SESSION_COUNTER.fetch_add(1, Ordering::AcqRel); debug!("new Session[{}]", session_id); @@ -130,7 +130,7 @@ impl Session { session_id, })); - let ap = session.apresolver().resolve("accesspoint").await; + let ap = session.apresolver().resolve("accesspoint").await?; info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); let mut transport = connection::connect(&ap.0, ap.1, session.config().proxy.as_ref()).await?; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index ffc2ebba..de57e97b 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -65,13 +65,13 @@ impl SpClient { self.lock(|inner| inner.accesspoint = None) } - pub async fn get_accesspoint(&self) -> SocketAddress { + pub async fn get_accesspoint(&self) -> Result { // Memoize the current access point. let ap = self.lock(|inner| inner.accesspoint.clone()); - match ap { + let tuple = match ap { Some(tuple) => tuple, None => { - let tuple = self.session().apresolver().resolve("spclient").await; + let tuple = self.session().apresolver().resolve("spclient").await?; self.lock(|inner| inner.accesspoint = Some(tuple.clone())); info!( "Resolved \"{}:{}\" as spclient access point", @@ -79,12 +79,13 @@ impl SpClient { ); tuple } - } + }; + Ok(tuple) } - pub async fn base_url(&self) -> String { - let ap = self.get_accesspoint().await; - format!("https://{}:{}", ap.0, ap.1) + pub async fn base_url(&self) -> Result { + let ap = self.get_accesspoint().await?; + Ok(format!("https://{}:{}", ap.0, ap.1)) } pub async fn request_with_protobuf( @@ -133,7 +134,7 @@ 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 url = self.base_url().await; + let mut url = self.base_url().await?; url.push_str(endpoint); // Add metrics. There is also an optional `partner` key with a value like diff --git a/playback/src/player.rs b/playback/src/player.rs index d5d4b269..a382b6c6 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -2057,24 +2057,23 @@ impl PlayerInternal { .. } = self.state { + let ping_time = stream_loader_controller.ping_time().as_secs_f32(); + // Request our read ahead range let request_data_length = max( - (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS - * stream_loader_controller.ping_time().as_secs_f32() - * bytes_per_second as f32) as usize, + (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS * ping_time * bytes_per_second as f32) + as usize, (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, ); - stream_loader_controller.fetch_next(request_data_length); - // Request the part we want to wait for blocking. This effecively means we wait for the previous request to partially complete. + // Request the part we want to wait for blocking. This effectively means we wait for the previous request to partially complete. let wait_for_data_length = max( - (READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS - * stream_loader_controller.ping_time().as_secs_f32() - * bytes_per_second as f32) as usize, + (READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS * ping_time * 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) + .fetch_next_and_wait(request_data_length, wait_for_data_length) .map_err(Into::into) } else { Ok(()) From cc9a574b2eef0999693e620e9871c54739d92903 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 5 Jan 2022 21:15:19 +0100 Subject: [PATCH 082/147] Move `ConnectConfig` to `connect` --- Cargo.lock | 2 +- connect/Cargo.toml | 7 --- connect/src/config.rs | 115 ++++++++++++++++++++++++++++++++++++++ connect/src/discovery.rs | 32 ----------- connect/src/lib.rs | 6 +- connect/src/spirc.rs | 7 +-- core/src/config.rs | 116 +-------------------------------------- core/src/spclient.rs | 7 ++- discovery/Cargo.toml | 4 ++ discovery/src/lib.rs | 3 +- discovery/src/server.rs | 5 +- src/main.rs | 8 +-- 12 files changed, 137 insertions(+), 175 deletions(-) create mode 100644 connect/src/config.rs delete mode 100644 connect/src/discovery.rs diff --git a/Cargo.lock b/Cargo.lock index a7f5093d..1b65127d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1291,7 +1291,6 @@ dependencies = [ "form_urlencoded", "futures-util", "librespot-core", - "librespot-discovery", "librespot-playback", "librespot-protocol", "log", @@ -1370,6 +1369,7 @@ dependencies = [ "hmac", "hyper", "libmdns", + "librespot-connect", "librespot-core", "log", "rand", diff --git a/connect/Cargo.toml b/connect/Cargo.toml index ab425a66..37521df9 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -30,10 +30,3 @@ version = "0.3.1" [dependencies.librespot-protocol] path = "../protocol" version = "0.3.1" - -[dependencies.librespot-discovery] -path = "../discovery" -version = "0.3.1" - -[features] -with-dns-sd = ["librespot-discovery/with-dns-sd"] diff --git a/connect/src/config.rs b/connect/src/config.rs new file mode 100644 index 00000000..4d751fcf --- /dev/null +++ b/connect/src/config.rs @@ -0,0 +1,115 @@ +use std::{fmt, str::FromStr}; + +#[derive(Clone, Debug)] +pub struct ConnectConfig { + pub name: String, + pub device_type: DeviceType, + pub initial_volume: Option, + pub has_volume_ctrl: 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, + } + } +} + +#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub enum DeviceType { + Unknown = 0, + Computer = 1, + Tablet = 2, + Smartphone = 3, + Speaker = 4, + Tv = 5, + Avr = 6, + Stb = 7, + AudioDongle = 8, + GameConsole = 9, + CastAudio = 10, + CastVideo = 11, + Automobile = 12, + Smartwatch = 13, + Chromebook = 14, + UnknownSpotify = 100, + CarThing = 101, + Observer = 102, + HomeThing = 103, +} + +impl FromStr for DeviceType { + type Err = (); + fn from_str(s: &str) -> Result { + use self::DeviceType::*; + match s.to_lowercase().as_ref() { + "computer" => Ok(Computer), + "tablet" => Ok(Tablet), + "smartphone" => Ok(Smartphone), + "speaker" => Ok(Speaker), + "tv" => Ok(Tv), + "avr" => Ok(Avr), + "stb" => Ok(Stb), + "audiodongle" => Ok(AudioDongle), + "gameconsole" => Ok(GameConsole), + "castaudio" => Ok(CastAudio), + "castvideo" => Ok(CastVideo), + "automobile" => Ok(Automobile), + "smartwatch" => Ok(Smartwatch), + "chromebook" => Ok(Chromebook), + "carthing" => Ok(CarThing), + "homething" => Ok(HomeThing), + _ => Err(()), + } + } +} + +impl From<&DeviceType> for &str { + fn from(d: &DeviceType) -> &'static str { + use self::DeviceType::*; + match d { + Unknown => "Unknown", + Computer => "Computer", + Tablet => "Tablet", + Smartphone => "Smartphone", + Speaker => "Speaker", + Tv => "TV", + Avr => "AVR", + Stb => "STB", + AudioDongle => "AudioDongle", + GameConsole => "GameConsole", + CastAudio => "CastAudio", + CastVideo => "CastVideo", + Automobile => "Automobile", + Smartwatch => "Smartwatch", + Chromebook => "Chromebook", + UnknownSpotify => "UnknownSpotify", + CarThing => "CarThing", + Observer => "Observer", + HomeThing => "HomeThing", + } + } +} + +impl From for &str { + fn from(d: DeviceType) -> &'static str { + (&d).into() + } +} + +impl fmt::Display for DeviceType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let str: &str = self.into(); + f.write_str(str) + } +} + +impl Default for DeviceType { + fn default() -> DeviceType { + DeviceType::Speaker + } +} diff --git a/connect/src/discovery.rs b/connect/src/discovery.rs deleted file mode 100644 index 8f4f9b34..00000000 --- a/connect/src/discovery.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::{ - io, - pin::Pin, - task::{Context, Poll}, -}; - -use futures_util::Stream; -use librespot_core::{authentication::Credentials, config::ConnectConfig}; - -pub struct DiscoveryStream(librespot_discovery::Discovery); - -impl Stream for DiscoveryStream { - type Item = Credentials; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.0).poll_next(cx) - } -} - -pub fn discovery( - config: ConnectConfig, - device_id: String, - port: u16, -) -> io::Result { - librespot_discovery::Discovery::builder(device_id) - .device_type(config.device_type) - .port(port) - .name(config.name) - .launch() - .map(DiscoveryStream) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) -} diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 267bf1b8..193e5db5 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -5,10 +5,6 @@ use librespot_core as core; use librespot_playback as playback; use librespot_protocol as protocol; +pub mod config; pub mod context; -#[deprecated( - since = "0.2.1", - note = "Please use the crate `librespot_discovery` instead." -)] -pub mod discovery; pub mod spirc; diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 427555ff..ef9da811 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -18,16 +18,13 @@ use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; use crate::{ + config::ConnectConfig, context::StationContext, core::{ - config::ConnectConfig, // TODO: move to connect? mercury::{MercuryError, MercurySender}, session::UserAttributes, util::SeqGenerator, - version, - Error, - Session, - SpotifyId, + version, Error, Session, SpotifyId, }, playback::{ mixer::Mixer, diff --git a/core/src/config.rs b/core/src/config.rs index b667a330..87c1637f 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -1,4 +1,4 @@ -use std::{fmt, path::PathBuf, str::FromStr}; +use std::path::PathBuf; use url::Url; @@ -21,117 +21,3 @@ impl Default for SessionConfig { } } } - -#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] -pub enum DeviceType { - Unknown = 0, - Computer = 1, - Tablet = 2, - Smartphone = 3, - Speaker = 4, - Tv = 5, - Avr = 6, - Stb = 7, - AudioDongle = 8, - GameConsole = 9, - CastAudio = 10, - CastVideo = 11, - Automobile = 12, - Smartwatch = 13, - Chromebook = 14, - UnknownSpotify = 100, - CarThing = 101, - Observer = 102, - HomeThing = 103, -} - -impl FromStr for DeviceType { - type Err = (); - fn from_str(s: &str) -> Result { - use self::DeviceType::*; - match s.to_lowercase().as_ref() { - "computer" => Ok(Computer), - "tablet" => Ok(Tablet), - "smartphone" => Ok(Smartphone), - "speaker" => Ok(Speaker), - "tv" => Ok(Tv), - "avr" => Ok(Avr), - "stb" => Ok(Stb), - "audiodongle" => Ok(AudioDongle), - "gameconsole" => Ok(GameConsole), - "castaudio" => Ok(CastAudio), - "castvideo" => Ok(CastVideo), - "automobile" => Ok(Automobile), - "smartwatch" => Ok(Smartwatch), - "chromebook" => Ok(Chromebook), - "carthing" => Ok(CarThing), - "homething" => Ok(HomeThing), - _ => Err(()), - } - } -} - -impl From<&DeviceType> for &str { - fn from(d: &DeviceType) -> &'static str { - use self::DeviceType::*; - match d { - Unknown => "Unknown", - Computer => "Computer", - Tablet => "Tablet", - Smartphone => "Smartphone", - Speaker => "Speaker", - Tv => "TV", - Avr => "AVR", - Stb => "STB", - AudioDongle => "AudioDongle", - GameConsole => "GameConsole", - CastAudio => "CastAudio", - CastVideo => "CastVideo", - Automobile => "Automobile", - Smartwatch => "Smartwatch", - Chromebook => "Chromebook", - UnknownSpotify => "UnknownSpotify", - CarThing => "CarThing", - Observer => "Observer", - HomeThing => "HomeThing", - } - } -} - -impl From for &str { - fn from(d: DeviceType) -> &'static str { - (&d).into() - } -} - -impl fmt::Display for DeviceType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let str: &str = self.into(); - f.write_str(str) - } -} - -impl Default for DeviceType { - fn default() -> DeviceType { - DeviceType::Speaker - } -} - -#[derive(Clone, Debug)] -pub struct ConnectConfig { - pub name: String, - pub device_type: DeviceType, - pub initial_volume: Option, - pub has_volume_ctrl: 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, - } - } -} diff --git a/core/src/spclient.rs b/core/src/spclient.rs index de57e97b..9985041a 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -98,7 +98,10 @@ 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, + HeaderValue::from_static("application/protobuf"), + ); self.request(method, endpoint, Some(headers), Some(body)) .await @@ -112,7 +115,7 @@ impl SpClient { body: Option, ) -> SpClientResult { let mut headers = headers.unwrap_or_else(HeaderMap::new); - headers.insert(ACCEPT, "application/json".parse()?); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); self.request(method, endpoint, Some(headers), body).await } diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 0225ab68..cafa6870 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -25,6 +25,10 @@ sha-1 = "0.9" thiserror = "1.0" tokio = { version = "1.0", features = ["parking_lot", "sync", "rt"] } +[dependencies.librespot-connect] +path = "../connect" +version = "0.3.1" + [dependencies.librespot-core] path = "../core" version = "0.3.1" diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index a29b3b8c..a4e124c5 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -16,6 +16,7 @@ use std::task::{Context, Poll}; use cfg_if::cfg_if; use futures_core::Stream; +use librespot_connect as connect; use librespot_core as core; use thiserror::Error; @@ -25,7 +26,7 @@ use self::server::DiscoveryServer; pub use crate::core::authentication::Credentials; /// Determining the icon in the list of available devices. -pub use crate::core::config::DeviceType; +pub use crate::connect::config::DeviceType; pub use crate::core::Error; diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 4a251ea5..b02c0e64 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -27,8 +27,9 @@ use tokio::sync::{mpsc, oneshot}; use super::DiscoveryError; -use crate::core::{ - authentication::Credentials, config::DeviceType, diffie_hellman::DhLocalKeys, Error, +use crate::{ + connect::config::DeviceType, + core::{authentication::Credentials, diffie_hellman::DhLocalKeys, Error}, }; type Params<'a> = BTreeMap, Cow<'a, str>>; diff --git a/src/main.rs b/src/main.rs index 8f2e532c..ff7c79da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,13 +18,11 @@ use tokio::sync::mpsc::UnboundedReceiver; use url::Url; use librespot::{ - connect::spirc::Spirc, - core::{ - authentication::Credentials, - cache::Cache, + connect::{ config::{ConnectConfig, DeviceType}, - version, Session, SessionConfig, + spirc::Spirc, }, + core::{authentication::Credentials, cache::Cache, version, Session, SessionConfig}, playback::{ audio_backend::{self, SinkBuilder, BACKENDS}, config::{ From 5d44f910f3fe00e0ab5e3427da008594b78902df Mon Sep 17 00:00:00 2001 From: Philip Deljanov Date: Wed, 5 Jan 2022 00:03:54 -0500 Subject: [PATCH 083/147] Handle format reset and decode errors. This change fixes two issues with the error handling of the Symphonia decode loop. 1) `Error::ResetRequired` should always be propagated to jump to the next Spotify track. 2) On a decode error, get a new packet and try again instead of propagating the error and jumping to the next track. --- playback/src/decoder/symphonia_decoder.rs | 67 ++++++++++++----------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 3b585007..fa096ade 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -171,43 +171,44 @@ impl AudioDecoder for SymphoniaDecoder { } fn next_packet(&mut self) -> DecoderResult> { - let packet = match self.format.next_packet() { - Ok(packet) => packet, - Err(Error::IoError(err)) => { - if err.kind() == io::ErrorKind::UnexpectedEof { - return Ok(None); - } else { - return Err(DecoderError::SymphoniaDecoder(err.to_string())); + loop { + let packet = match self.format.next_packet() { + Ok(packet) => packet, + Err(Error::IoError(err)) => { + if err.kind() == io::ErrorKind::UnexpectedEof { + return Ok(None); + } else { + return Err(DecoderError::SymphoniaDecoder(err.to_string())); + } } - } - Err(Error::ResetRequired) => { - self.decoder.reset(); - return self.next_packet(); - } - Err(err) => { - return Err(err.into()); - } - }; - - let position_ms = self.ts_to_ms(packet.pts()); - - match self.decoder.decode(&packet) { - Ok(decoded) => { - if self.sample_buffer.is_none() { - let spec = *decoded.spec(); - let duration = decoded.capacity() as u64; - self.sample_buffer - .replace(SampleBuffer::new(duration, spec)); + Err(err) => { + return Err(err.into()); } + }; - let sample_buffer = self.sample_buffer.as_mut().unwrap(); // guaranteed above - sample_buffer.copy_interleaved_ref(decoded); - let samples = AudioPacket::Samples(sample_buffer.samples().to_vec()); - Ok(Some((position_ms, samples))) + let position_ms = self.ts_to_ms(packet.pts()); + + match self.decoder.decode(&packet) { + Ok(decoded) => { + if self.sample_buffer.is_none() { + let spec = *decoded.spec(); + let duration = decoded.capacity() as u64; + self.sample_buffer + .replace(SampleBuffer::new(duration, spec)); + } + + let sample_buffer = self.sample_buffer.as_mut().unwrap(); // guaranteed above + sample_buffer.copy_interleaved_ref(decoded); + let samples = AudioPacket::Samples(sample_buffer.samples().to_vec()); + return Ok(Some((position_ms, samples))); + } + Err(Error::DecodeError(_)) => { + // The packet failed to decode due to corrupted or invalid data, get a new + // packet and try again. + continue; + } + Err(err) => return Err(err.into()), } - // Also propagate `ResetRequired` errors from the decoder to the player, - // so that it will skip to the next track and reload the entire Symphonia decoder. - Err(err) => Err(err.into()), } } } From 4ca1f661d59dfe5288debb37d1805b993b6d9185 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 6 Jan 2022 09:43:50 +0100 Subject: [PATCH 084/147] Prevent deadlock --- core/src/apresolve.rs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 1e1c6de6..72b089dd 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,8 +1,3 @@ -use std::{ - hint, - sync::atomic::{AtomicBool, Ordering}, -}; - use hyper::{Body, Method, Request}; use serde::Deserialize; @@ -40,7 +35,6 @@ impl Default for ApResolveData { component! { ApResolver : ApResolverInner { data: AccessPoints = AccessPoints::default(), - in_progress: AtomicBool = AtomicBool::new(false), } } @@ -111,16 +105,6 @@ impl ApResolver { } pub async fn resolve(&self, endpoint: &str) -> Result { - // Use a spinlock to make this function atomic. Otherwise, various race conditions may - // occur, e.g. when the session is created, multiple components are launched almost in - // parallel and they will all call this function, while resolving is still in progress. - self.lock(|inner| { - while inner.in_progress.load(Ordering::Acquire) { - hint::spin_loop(); - } - inner.in_progress.store(true, Ordering::Release); - }); - if self.is_empty() { self.apresolve().await; } @@ -140,7 +124,7 @@ impl ApResolver { ))) } }; - inner.in_progress.store(false, Ordering::Release); + Ok(access_point) }) } From c1965198fc714e8ae39b24516a4a2bedc062bb05 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 6 Jan 2022 09:48:11 +0100 Subject: [PATCH 085/147] Move `DeviceType` to `core` --- Cargo.lock | 1 - connect/src/config.rs | 97 +---------------------------------------- core/src/config.rs | 97 ++++++++++++++++++++++++++++++++++++++++- discovery/Cargo.toml | 4 -- discovery/src/lib.rs | 19 ++++---- discovery/src/server.rs | 2 +- src/main.rs | 8 ++-- 7 files changed, 112 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b65127d..dddd26a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1369,7 +1369,6 @@ dependencies = [ "hmac", "hyper", "libmdns", - "librespot-connect", "librespot-core", "log", "rand", diff --git a/connect/src/config.rs b/connect/src/config.rs index 4d751fcf..473fa173 100644 --- a/connect/src/config.rs +++ b/connect/src/config.rs @@ -1,4 +1,4 @@ -use std::{fmt, str::FromStr}; +use crate::core::config::DeviceType; #[derive(Clone, Debug)] pub struct ConnectConfig { @@ -18,98 +18,3 @@ impl Default for ConnectConfig { } } } - -#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] -pub enum DeviceType { - Unknown = 0, - Computer = 1, - Tablet = 2, - Smartphone = 3, - Speaker = 4, - Tv = 5, - Avr = 6, - Stb = 7, - AudioDongle = 8, - GameConsole = 9, - CastAudio = 10, - CastVideo = 11, - Automobile = 12, - Smartwatch = 13, - Chromebook = 14, - UnknownSpotify = 100, - CarThing = 101, - Observer = 102, - HomeThing = 103, -} - -impl FromStr for DeviceType { - type Err = (); - fn from_str(s: &str) -> Result { - use self::DeviceType::*; - match s.to_lowercase().as_ref() { - "computer" => Ok(Computer), - "tablet" => Ok(Tablet), - "smartphone" => Ok(Smartphone), - "speaker" => Ok(Speaker), - "tv" => Ok(Tv), - "avr" => Ok(Avr), - "stb" => Ok(Stb), - "audiodongle" => Ok(AudioDongle), - "gameconsole" => Ok(GameConsole), - "castaudio" => Ok(CastAudio), - "castvideo" => Ok(CastVideo), - "automobile" => Ok(Automobile), - "smartwatch" => Ok(Smartwatch), - "chromebook" => Ok(Chromebook), - "carthing" => Ok(CarThing), - "homething" => Ok(HomeThing), - _ => Err(()), - } - } -} - -impl From<&DeviceType> for &str { - fn from(d: &DeviceType) -> &'static str { - use self::DeviceType::*; - match d { - Unknown => "Unknown", - Computer => "Computer", - Tablet => "Tablet", - Smartphone => "Smartphone", - Speaker => "Speaker", - Tv => "TV", - Avr => "AVR", - Stb => "STB", - AudioDongle => "AudioDongle", - GameConsole => "GameConsole", - CastAudio => "CastAudio", - CastVideo => "CastVideo", - Automobile => "Automobile", - Smartwatch => "Smartwatch", - Chromebook => "Chromebook", - UnknownSpotify => "UnknownSpotify", - CarThing => "CarThing", - Observer => "Observer", - HomeThing => "HomeThing", - } - } -} - -impl From for &str { - fn from(d: DeviceType) -> &'static str { - (&d).into() - } -} - -impl fmt::Display for DeviceType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let str: &str = self.into(); - f.write_str(str) - } -} - -impl Default for DeviceType { - fn default() -> DeviceType { - DeviceType::Speaker - } -} diff --git a/core/src/config.rs b/core/src/config.rs index 87c1637f..4c1b1dd8 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{fmt, path::PathBuf, str::FromStr}; use url::Url; @@ -21,3 +21,98 @@ impl Default for SessionConfig { } } } + +#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub enum DeviceType { + Unknown = 0, + Computer = 1, + Tablet = 2, + Smartphone = 3, + Speaker = 4, + Tv = 5, + Avr = 6, + Stb = 7, + AudioDongle = 8, + GameConsole = 9, + CastAudio = 10, + CastVideo = 11, + Automobile = 12, + Smartwatch = 13, + Chromebook = 14, + UnknownSpotify = 100, + CarThing = 101, + Observer = 102, + HomeThing = 103, +} + +impl FromStr for DeviceType { + type Err = (); + fn from_str(s: &str) -> Result { + use self::DeviceType::*; + match s.to_lowercase().as_ref() { + "computer" => Ok(Computer), + "tablet" => Ok(Tablet), + "smartphone" => Ok(Smartphone), + "speaker" => Ok(Speaker), + "tv" => Ok(Tv), + "avr" => Ok(Avr), + "stb" => Ok(Stb), + "audiodongle" => Ok(AudioDongle), + "gameconsole" => Ok(GameConsole), + "castaudio" => Ok(CastAudio), + "castvideo" => Ok(CastVideo), + "automobile" => Ok(Automobile), + "smartwatch" => Ok(Smartwatch), + "chromebook" => Ok(Chromebook), + "carthing" => Ok(CarThing), + "homething" => Ok(HomeThing), + _ => Err(()), + } + } +} + +impl From<&DeviceType> for &str { + fn from(d: &DeviceType) -> &'static str { + use self::DeviceType::*; + match d { + Unknown => "Unknown", + Computer => "Computer", + Tablet => "Tablet", + Smartphone => "Smartphone", + Speaker => "Speaker", + Tv => "TV", + Avr => "AVR", + Stb => "STB", + AudioDongle => "AudioDongle", + GameConsole => "GameConsole", + CastAudio => "CastAudio", + CastVideo => "CastVideo", + Automobile => "Automobile", + Smartwatch => "Smartwatch", + Chromebook => "Chromebook", + UnknownSpotify => "UnknownSpotify", + CarThing => "CarThing", + Observer => "Observer", + HomeThing => "HomeThing", + } + } +} + +impl From for &str { + fn from(d: DeviceType) -> &'static str { + (&d).into() + } +} + +impl fmt::Display for DeviceType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let str: &str = self.into(); + f.write_str(str) + } +} + +impl Default for DeviceType { + fn default() -> DeviceType { + DeviceType::Speaker + } +} diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index cafa6870..0225ab68 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -25,10 +25,6 @@ sha-1 = "0.9" thiserror = "1.0" tokio = { version = "1.0", features = ["parking_lot", "sync", "rt"] } -[dependencies.librespot-connect] -path = "../connect" -version = "0.3.1" - [dependencies.librespot-core] path = "../core" version = "0.3.1" diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index a4e124c5..b4e95737 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -9,26 +9,27 @@ mod server; -use std::borrow::Cow; -use std::io; -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + borrow::Cow, + io, + pin::Pin, + task::{Context, Poll}, +}; use cfg_if::cfg_if; use futures_core::Stream; -use librespot_connect as connect; -use librespot_core as core; use thiserror::Error; use self::server::DiscoveryServer; +pub use crate::core::Error; +use librespot_core as core; + /// Credentials to be used in [`librespot`](`librespot_core`). pub use crate::core::authentication::Credentials; /// Determining the icon in the list of available devices. -pub use crate::connect::config::DeviceType; - -pub use crate::core::Error; +pub use crate::core::config::DeviceType; /// Makes this device visible to Spotify clients in the local network. /// diff --git a/discovery/src/server.rs b/discovery/src/server.rs index b02c0e64..9cf6837b 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -28,7 +28,7 @@ use tokio::sync::{mpsc, oneshot}; use super::DiscoveryError; use crate::{ - connect::config::DeviceType, + core::config::DeviceType, core::{authentication::Credentials, diffie_hellman::DhLocalKeys, Error}, }; diff --git a/src/main.rs b/src/main.rs index ff7c79da..2d0337cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,11 +18,11 @@ use tokio::sync::mpsc::UnboundedReceiver; use url::Url; use librespot::{ - connect::{ - config::{ConnectConfig, DeviceType}, - spirc::Spirc, + connect::{config::ConnectConfig, spirc::Spirc}, + core::{ + authentication::Credentials, cache::Cache, config::DeviceType, version, Session, + SessionConfig, }, - core::{authentication::Credentials, cache::Cache, version, Session, SessionConfig}, playback::{ audio_backend::{self, SinkBuilder, BACKENDS}, config::{ From 8d74d48809f5440fae3aaaf13a29ac8b0321c5a9 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 6 Jan 2022 21:55:08 +0100 Subject: [PATCH 086/147] Audio file seeking improvements - Ensure there is enough disk space for the write file - Switch streaming mode only if necessary - Return `Err` on seeking errors, instead of exiting - Use the actual position after seeking --- audio/src/fetch/mod.rs | 47 ++++++++++++- playback/src/decoder/mod.rs | 6 ++ playback/src/player.rs | 133 ++++++++++-------------------------- 3 files changed, 86 insertions(+), 100 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index ad1b98e1..0bc1f74c 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -325,12 +325,18 @@ struct AudioFileDownloadStatus { downloaded: RangeSet, } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] enum DownloadStrategy { RandomAccess(), Streaming(), } +impl Default for DownloadStrategy { + fn default() -> Self { + Self::Streaming() + } +} + struct AudioFileShared { cdn_url: CdnUrl, file_size: usize, @@ -456,13 +462,15 @@ impl AudioFileStreaming { requested: RangeSet::new(), downloaded: RangeSet::new(), }), - download_strategy: Mutex::new(DownloadStrategy::Streaming()), + download_strategy: Mutex::new(DownloadStrategy::default()), number_of_open_requests: AtomicUsize::new(0), ping_time_ms: AtomicUsize::new(INITIAL_PING_TIME_ESTIMATE.as_millis() as usize), read_position: AtomicUsize::new(0), }); let write_file = NamedTempFile::new_in(session.config().tmp_dir.clone())?; + write_file.as_file().set_len(file_size as u64)?; + let read_file = write_file.reopen()?; let (stream_loader_command_tx, stream_loader_command_rx) = @@ -567,11 +575,44 @@ impl Read for AudioFileStreaming { impl Seek for AudioFileStreaming { fn seek(&mut self, pos: SeekFrom) -> io::Result { + // If we are already at this position, we don't need to switch download strategy. + // These checks and locks are less expensive than interrupting streaming. + let current_position = self.position as i64; + let requested_pos = match pos { + SeekFrom::Start(pos) => pos as i64, + SeekFrom::End(pos) => self.shared.file_size as i64 - pos - 1, + SeekFrom::Current(pos) => current_position + pos, + }; + if requested_pos == current_position { + return Ok(current_position as u64); + } + + // Again if we have already downloaded this part. + let available = self + .shared + .download_status + .lock() + .downloaded + .contains(requested_pos as usize); + + let mut old_strategy = DownloadStrategy::default(); + if !available { + // Ensure random access mode if we need to download this part. + old_strategy = std::mem::replace( + &mut *(self.shared.download_strategy.lock()), + DownloadStrategy::RandomAccess(), + ); + } + self.position = self.read_file.seek(pos)?; - // Do not seek past EOF self.shared .read_position .store(self.position as usize, Ordering::Release); + + if !available && old_strategy != DownloadStrategy::RandomAccess() { + *(self.shared.download_strategy.lock()) = old_strategy; + } + Ok(self.position) } } diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index 8d4cc1c8..e74f92b7 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -59,6 +59,12 @@ pub trait AudioDecoder { fn next_packet(&mut self) -> DecoderResult>; } +impl From for librespot_core::error::Error { + fn from(err: DecoderError) -> Self { + librespot_core::error::Error::aborted(err) + } +} + impl From for DecoderError { fn from(err: symphonia::core::errors::Error) -> Self { Self::SymphoniaDecoder(err.to_string()) diff --git a/playback/src/player.rs b/playback/src/player.rs index a382b6c6..56c57334 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -591,25 +591,6 @@ impl PlayerState { } } - fn stream_loader_controller(&mut self) -> Option<&mut StreamLoaderController> { - use self::PlayerState::*; - match *self { - Stopped | EndOfTrack { .. } | Loading { .. } => None, - Paused { - ref mut stream_loader_controller, - .. - } - | Playing { - ref mut stream_loader_controller, - .. - } => Some(stream_loader_controller), - Invalid => { - error!("PlayerState::stream_loader_controller in invalid state"); - exit(1); - } - } - } - fn playing_to_end_of_track(&mut self) { use self::PlayerState::*; let new_state = mem::replace(self, Invalid); @@ -891,9 +872,7 @@ impl PlayerTrackLoader { let is_cached = encrypted_file.is_cached(); - // Setting up demuxing and decoding will trigger a seek() so always start in random access mode. let stream_loader_controller = encrypted_file.get_stream_loader_controller().ok()?; - stream_loader_controller.set_random_access_mode(); // Not all audio files are encrypted. If we can't get a key, try loading the track // without decryption. If the file was encrypted after all, the decoder will fail @@ -917,11 +896,17 @@ impl PlayerTrackLoader { (0, None) }; - let audio_file = Subfile::new( + let audio_file = match Subfile::new( decrypted_file, offset, stream_loader_controller.len() as u64, - ); + ) { + Ok(audio_file) => audio_file, + Err(e) => { + error!("PlayerTrackLoader::load_track error opening subfile: {}", e); + return None; + } + }; let result = if self.config.passthrough { PassthroughDecoder::new(audio_file, format).map(|x| Box::new(x) as Decoder) @@ -976,18 +961,17 @@ impl PlayerTrackLoader { // matter for playback (but won't hurt either), but may be useful for the // passthrough decoder. let stream_position_ms = match decoder.seek(position_ms) { - Ok(_) => position_ms, + Ok(new_position_ms) => new_position_ms, Err(e) => { - warn!( - "PlayerTrackLoader::load_track error seeking to {}: {}", + error!( + "PlayerTrackLoader::load_track error seeking to starting position {}: {}", position_ms, e ); - 0 + return None; } }; - // Transition from random access mode to streaming mode now that - // we are ready to play from the requested position. + // Ensure streaming mode now that we are ready to play from the requested position. stream_loader_controller.set_stream_mode(); info!("<{}> ({} ms) loaded", audio.name, audio.duration); @@ -1585,7 +1569,7 @@ impl PlayerInternal { play_request_id: u64, play: bool, position_ms: u32, - ) { + ) -> PlayerResult { if !self.config.gapless { self.ensure_sink_stopped(play); } @@ -1616,11 +1600,10 @@ impl PlayerInternal { position_ms, }), PlayerState::Invalid { .. } => { - error!( + return Err(Error::internal(format!( "Player::handle_command_load called from invalid state: {:?}", self.state - ); - exit(1); + ))); } } @@ -1638,29 +1621,20 @@ impl PlayerInternal { let mut loaded_track = match mem::replace(&mut self.state, PlayerState::Invalid) { PlayerState::EndOfTrack { loaded_track, .. } => loaded_track, _ => { - error!("PlayerInternal handle_command_load: Invalid PlayerState"); - exit(1); + return Err(Error::internal(format!("PlayerInternal::handle_command_load repeating the same track: invalid state: {:?}", self.state))); } }; if position_ms != loaded_track.stream_position_ms { - loaded_track - .stream_loader_controller - .set_random_access_mode(); // This may be blocking. - match loaded_track.decoder.seek(position_ms) { - Ok(_) => loaded_track.stream_position_ms = position_ms, - Err(e) => error!("PlayerInternal handle_command_load: {}", e), - } - loaded_track.stream_loader_controller.set_stream_mode(); + loaded_track.stream_position_ms = loaded_track.decoder.seek(position_ms)?; } self.preload = PlayerPreload::None; self.start_playback(track_id, play_request_id, loaded_track, play); if let PlayerState::Invalid = self.state { - error!("start_playback() hasn't set a valid player state."); - exit(1); + return Err(Error::internal(format!("PlayerInternal::handle_command_load repeating the same track: start_playback() did not transition to valid player state: {:?}", self.state))); } - return; + return Ok(()); } } @@ -1669,29 +1643,20 @@ impl PlayerInternal { track_id: current_track_id, ref mut stream_position_ms, ref mut decoder, - ref mut stream_loader_controller, .. } | PlayerState::Paused { track_id: current_track_id, ref mut stream_position_ms, ref mut decoder, - ref mut stream_loader_controller, .. } = self.state { if current_track_id == track_id { // we can use the current decoder. Ensure it's at the correct position. if position_ms != *stream_position_ms { - stream_loader_controller.set_random_access_mode(); // This may be blocking. - match decoder.seek(position_ms) { - Ok(_) => *stream_position_ms = position_ms, - Err(e) => { - error!("PlayerInternal::handle_command_load error seeking: {}", e) - } - } - stream_loader_controller.set_stream_mode(); + *stream_position_ms = decoder.seek(position_ms)?; } // Move the info from the current state into a PlayerLoadedTrackData so we can use @@ -1733,14 +1698,12 @@ impl PlayerInternal { self.start_playback(track_id, play_request_id, loaded_track, play); if let PlayerState::Invalid = self.state { - error!("start_playback() hasn't set a valid player state."); - exit(1); + return Err(Error::internal(format!("PlayerInternal::handle_command_load already playing this track: start_playback() did not transition to valid player state: {:?}", self.state))); } - return; + return Ok(()); } else { - error!("PlayerInternal handle_command_load: Invalid PlayerState"); - exit(1); + return Err(Error::internal(format!("PlayerInternal::handle_command_load already playing this track: invalid state: {:?}", self.state))); } } } @@ -1759,21 +1722,13 @@ impl PlayerInternal { } = preload { if position_ms != loaded_track.stream_position_ms { - loaded_track - .stream_loader_controller - .set_random_access_mode(); // This may be blocking - match loaded_track.decoder.seek(position_ms) { - Ok(_) => loaded_track.stream_position_ms = position_ms, - Err(e) => error!("PlayerInternal handle_command_load: {}", e), - } - loaded_track.stream_loader_controller.set_stream_mode(); + loaded_track.stream_position_ms = loaded_track.decoder.seek(position_ms)?; } self.start_playback(track_id, play_request_id, *loaded_track, play); - return; + return Ok(()); } else { - error!("PlayerInternal handle_command_load: Invalid PlayerState"); - exit(1); + return Err(Error::internal(format!("PlayerInternal::handle_command_loading preloaded track: invalid state: {:?}", self.state))); } } } @@ -1821,6 +1776,8 @@ impl PlayerInternal { start_playback: play, loader, }; + + Ok(()) } fn handle_command_preload(&mut self, track_id: SpotifyId) { @@ -1875,12 +1832,9 @@ impl PlayerInternal { } 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(); - } if let Some(decoder) = self.state.decoder() { match decoder.seek(position_ms) { - Ok(_) => { + Ok(new_position_ms) => { if let PlayerState::Playing { ref mut stream_position_ms, .. @@ -1890,7 +1844,7 @@ impl PlayerInternal { .. } = self.state { - *stream_position_ms = position_ms; + *stream_position_ms = new_position_ms; } } Err(e) => error!("PlayerInternal::handle_command_seek error: {}", e), @@ -1899,11 +1853,6 @@ impl PlayerInternal { 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. - if let Some(stream_loader_controller) = self.state.stream_loader_controller() { - stream_loader_controller.set_stream_mode(); - } - // ensure we have a bit of a buffer of downloaded data self.preload_data_before_playback()?; @@ -1950,7 +1899,7 @@ impl PlayerInternal { play_request_id, play, position_ms, - } => self.handle_command_load(track_id, play_request_id, play, position_ms), + } => self.handle_command_load(track_id, play_request_id, play, position_ms)?, PlayerCommand::Preload { track_id } => self.handle_command_preload(track_id), @@ -2191,25 +2140,15 @@ struct Subfile { } impl Subfile { - pub fn new(mut stream: T, offset: u64, length: u64) -> Subfile { + pub fn new(mut stream: T, offset: u64, length: u64) -> Result, io::Error> { 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), - } + stream.seek(target)?; - Subfile { + Ok(Subfile { stream, offset, length, - } + }) } } From 67ae0fcf8de62d90b7074c4f1c8403c9d1702349 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 6 Jan 2022 22:11:53 +0100 Subject: [PATCH 087/147] Fix gapless playback --- playback/src/player.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 56c57334..d3fe5aca 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1104,6 +1104,8 @@ impl Future for PlayerInternal { match decoder.next_packet() { Ok(result) => { if let Some((new_stream_position_ms, ref packet)) = result { + *stream_position_ms = new_stream_position_ms; + if !passthrough { match packet.samples() { Ok(_) => { @@ -1144,9 +1146,6 @@ impl Future for PlayerInternal { }) } } - } else { - // position, even if irrelevant, must be set so that seek() is called - *stream_position_ms = new_stream_position_ms; } } From a33014f9c5ae40125098a28394ffc7fc034e8f63 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 6 Jan 2022 22:46:50 +0100 Subject: [PATCH 088/147] Notify track position after skipping malformed packets --- playback/src/decoder/mod.rs | 26 ++++++++++++++++++++- playback/src/decoder/passthrough_decoder.rs | 14 ++++++++--- playback/src/decoder/symphonia_decoder.rs | 17 +++++++++++--- playback/src/player.rs | 23 +++++++++++++----- 4 files changed, 67 insertions(+), 13 deletions(-) diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index e74f92b7..05279c1b 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -1,3 +1,5 @@ +use std::ops::Deref; + use thiserror::Error; mod passthrough_decoder; @@ -54,9 +56,31 @@ impl AudioPacket { } } +#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum AudioPositionKind { + // the position is at the expected packet + Current, + // the decoder skipped some corrupted or invalid data, + // and the position is now later than expected + SkippedTo, +} + +#[derive(Debug, Clone)] +pub struct AudioPacketPosition { + pub position_ms: u32, + pub kind: AudioPositionKind, +} + +impl Deref for AudioPacketPosition { + type Target = u32; + fn deref(&self) -> &Self::Target { + &self.position_ms + } +} + pub trait AudioDecoder { fn seek(&mut self, position_ms: u32) -> Result; - fn next_packet(&mut self) -> DecoderResult>; + fn next_packet(&mut self) -> DecoderResult>; } impl From for librespot_core::error::Error { diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index 80a649f2..ec3a7753 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -7,7 +7,9 @@ use std::{ // TODO: move this to the Symphonia Ogg demuxer use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; -use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; +use super::{ + AudioDecoder, AudioPacket, AudioPacketPosition, AudioPositionKind, DecoderError, DecoderResult, +}; use crate::{ metadata::audio::{AudioFileFormat, AudioFiles}, @@ -137,7 +139,7 @@ impl AudioDecoder for PassthroughDecoder { } } - fn next_packet(&mut self) -> DecoderResult> { + fn next_packet(&mut self) -> DecoderResult> { // write headers if we are (re)starting if !self.bos { self.wtr @@ -208,8 +210,14 @@ impl AudioDecoder for PassthroughDecoder { if !data.is_empty() { let position_ms = Self::position_pcm_to_ms(pckgp_page); + let packet_position = AudioPacketPosition { + position_ms, + kind: AudioPositionKind::Current, + }; + let ogg_data = AudioPacket::Raw(std::mem::take(data)); - return Ok(Some((position_ms, ogg_data))); + + return Ok(Some((packet_position, ogg_data))); } } } diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index fa096ade..049e4998 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -16,7 +16,9 @@ use symphonia::{ }, }; -use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; +use super::{ + AudioDecoder, AudioPacket, AudioPacketPosition, AudioPositionKind, DecoderError, DecoderResult, +}; use crate::{ metadata::audio::{AudioFileFormat, AudioFiles}, @@ -170,7 +172,9 @@ impl AudioDecoder for SymphoniaDecoder { Ok(self.ts_to_ms(seeked_to_ts.actual_ts)) } - fn next_packet(&mut self) -> DecoderResult> { + fn next_packet(&mut self) -> DecoderResult> { + let mut position_kind = AudioPositionKind::Current; + loop { let packet = match self.format.next_packet() { Ok(packet) => packet, @@ -187,6 +191,10 @@ impl AudioDecoder for SymphoniaDecoder { }; let position_ms = self.ts_to_ms(packet.pts()); + let packet_position = AudioPacketPosition { + position_ms, + kind: position_kind, + }; match self.decoder.decode(&packet) { Ok(decoded) => { @@ -200,11 +208,14 @@ impl AudioDecoder for SymphoniaDecoder { let sample_buffer = self.sample_buffer.as_mut().unwrap(); // guaranteed above sample_buffer.copy_interleaved_ref(decoded); let samples = AudioPacket::Samples(sample_buffer.samples().to_vec()); - return Ok(Some((position_ms, samples))); + + return Ok(Some((packet_position, samples))); } Err(Error::DecodeError(_)) => { // The packet failed to decode due to corrupted or invalid data, get a new // packet and try again. + warn!("Skipping malformed audio packet at {} ms", position_ms); + position_kind = AudioPositionKind::SkippedTo; continue; } Err(err) => return Err(err.into()), diff --git a/playback/src/player.rs b/playback/src/player.rs index d3fe5aca..f8319798 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -29,7 +29,10 @@ use crate::{ config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, convert::Converter, core::{util::SeqGenerator, Error, Session, SpotifyId}, - decoder::{AudioDecoder, AudioPacket, PassthroughDecoder, SymphoniaDecoder}, + decoder::{ + AudioDecoder, AudioPacket, AudioPacketPosition, AudioPositionKind, PassthroughDecoder, + SymphoniaDecoder, + }, metadata::audio::{AudioFileFormat, AudioFiles, AudioItem}, mixer::AudioFilter, }; @@ -1103,17 +1106,21 @@ impl Future for PlayerInternal { { match decoder.next_packet() { Ok(result) => { - if let Some((new_stream_position_ms, ref packet)) = result { + if let Some((ref packet_position, ref packet)) = result { + let new_stream_position_ms = packet_position.position_ms; *stream_position_ms = new_stream_position_ms; if !passthrough { match packet.samples() { Ok(_) => { - let notify_about_position = - match *reported_nominal_start_time { + // Only notify if we're skipped some packets *or* we are behind. + // If we're ahead it's probably due to a buffer of the backend + // and we're actually in time. + let notify_about_position = packet_position.kind + != AudioPositionKind::Current + || 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() @@ -1340,7 +1347,11 @@ impl PlayerInternal { } } - fn handle_packet(&mut self, packet: Option<(u32, AudioPacket)>, normalisation_factor: f64) { + fn handle_packet( + &mut self, + packet: Option<(AudioPacketPosition, AudioPacket)>, + normalisation_factor: f64, + ) { match packet { Some((_, mut packet)) => { if !packet.is_empty() { From d380f1f04049719e6d973baed0fb57dbacad790c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 7 Jan 2022 11:13:23 +0100 Subject: [PATCH 089/147] Simplify `AudioPacketPosition` --- playback/src/decoder/mod.rs | 11 +---------- playback/src/decoder/passthrough_decoder.rs | 6 ++---- playback/src/decoder/symphonia_decoder.rs | 10 ++++------ playback/src/player.rs | 6 ++---- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index 05279c1b..2526da34 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -56,19 +56,10 @@ impl AudioPacket { } } -#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] -pub enum AudioPositionKind { - // the position is at the expected packet - Current, - // the decoder skipped some corrupted or invalid data, - // and the position is now later than expected - SkippedTo, -} - #[derive(Debug, Clone)] pub struct AudioPacketPosition { pub position_ms: u32, - pub kind: AudioPositionKind, + pub skipped: bool, } impl Deref for AudioPacketPosition { diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index ec3a7753..b04b8e0d 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -7,9 +7,7 @@ use std::{ // TODO: move this to the Symphonia Ogg demuxer use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; -use super::{ - AudioDecoder, AudioPacket, AudioPacketPosition, AudioPositionKind, DecoderError, DecoderResult, -}; +use super::{AudioDecoder, AudioPacket, AudioPacketPosition, DecoderError, DecoderResult}; use crate::{ metadata::audio::{AudioFileFormat, AudioFiles}, @@ -212,7 +210,7 @@ impl AudioDecoder for PassthroughDecoder { let position_ms = Self::position_pcm_to_ms(pckgp_page); let packet_position = AudioPacketPosition { position_ms, - kind: AudioPositionKind::Current, + skipped: false, }; let ogg_data = AudioPacket::Raw(std::mem::take(data)); diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 049e4998..27cb9e83 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -16,9 +16,7 @@ use symphonia::{ }, }; -use super::{ - AudioDecoder, AudioPacket, AudioPacketPosition, AudioPositionKind, DecoderError, DecoderResult, -}; +use super::{AudioDecoder, AudioPacket, AudioPacketPosition, DecoderError, DecoderResult}; use crate::{ metadata::audio::{AudioFileFormat, AudioFiles}, @@ -173,7 +171,7 @@ impl AudioDecoder for SymphoniaDecoder { } fn next_packet(&mut self) -> DecoderResult> { - let mut position_kind = AudioPositionKind::Current; + let mut skipped = false; loop { let packet = match self.format.next_packet() { @@ -193,7 +191,7 @@ impl AudioDecoder for SymphoniaDecoder { let position_ms = self.ts_to_ms(packet.pts()); let packet_position = AudioPacketPosition { position_ms, - kind: position_kind, + skipped, }; match self.decoder.decode(&packet) { @@ -215,7 +213,7 @@ impl AudioDecoder for SymphoniaDecoder { // The packet failed to decode due to corrupted or invalid data, get a new // packet and try again. warn!("Skipping malformed audio packet at {} ms", position_ms); - position_kind = AudioPositionKind::SkippedTo; + skipped = true; continue; } Err(err) => return Err(err.into()), diff --git a/playback/src/player.rs b/playback/src/player.rs index f8319798..cfa4414e 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -30,8 +30,7 @@ use crate::{ convert::Converter, core::{util::SeqGenerator, Error, Session, SpotifyId}, decoder::{ - AudioDecoder, AudioPacket, AudioPacketPosition, AudioPositionKind, PassthroughDecoder, - SymphoniaDecoder, + AudioDecoder, AudioPacket, AudioPacketPosition, PassthroughDecoder, SymphoniaDecoder, }, metadata::audio::{AudioFileFormat, AudioFiles, AudioItem}, mixer::AudioFilter, @@ -1116,8 +1115,7 @@ impl Future for PlayerInternal { // Only notify if we're skipped some packets *or* we are behind. // If we're ahead it's probably due to a buffer of the backend // and we're actually in time. - let notify_about_position = packet_position.kind - != AudioPositionKind::Current + let notify_about_position = packet_position.skipped || match *reported_nominal_start_time { None => true, Some(reported_nominal_start_time) => { From 62ccdbc580e5e42f3abe7b2677c9d967f6c1d31f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 7 Jan 2022 11:38:24 +0100 Subject: [PATCH 090/147] Improve performance of getting/setting download mode --- audio/src/fetch/mod.rs | 87 ++++++++++++++++++-------------------- audio/src/fetch/receive.rs | 30 +++++++------ 2 files changed, 56 insertions(+), 61 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 0bc1f74c..4a7742ec 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -5,7 +5,7 @@ use std::{ fs, io::{self, Read, Seek, SeekFrom}, sync::{ - atomic::{AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, }, time::{Duration, Instant}, @@ -137,10 +137,10 @@ pub struct StreamingRequest { #[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 - Close(), // terminate and don't load any more data + 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 + Close, // terminate and don't load any more data } #[derive(Clone)] @@ -299,17 +299,17 @@ impl StreamLoaderController { pub fn set_random_access_mode(&self) { // optimise download strategy for random access - self.send_stream_loader_command(StreamLoaderCommand::RandomAccessMode()); + self.send_stream_loader_command(StreamLoaderCommand::RandomAccessMode); } pub fn set_stream_mode(&self) { // optimise download strategy for streaming - self.send_stream_loader_command(StreamLoaderCommand::StreamMode()); + self.send_stream_loader_command(StreamLoaderCommand::StreamMode); } pub fn close(&self) { // terminate stream loading and don't load any more data for this file. - self.send_stream_loader_command(StreamLoaderCommand::Close()); + self.send_stream_loader_command(StreamLoaderCommand::Close); } } @@ -325,25 +325,13 @@ struct AudioFileDownloadStatus { downloaded: RangeSet, } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum DownloadStrategy { - RandomAccess(), - Streaming(), -} - -impl Default for DownloadStrategy { - fn default() -> Self { - Self::Streaming() - } -} - struct AudioFileShared { cdn_url: CdnUrl, file_size: usize, bytes_per_second: usize, cond: Condvar, download_status: Mutex, - download_strategy: Mutex, + download_streaming: AtomicBool, number_of_open_requests: AtomicUsize, ping_time_ms: AtomicUsize, read_position: AtomicUsize, @@ -462,7 +450,7 @@ impl AudioFileStreaming { requested: RangeSet::new(), downloaded: RangeSet::new(), }), - download_strategy: Mutex::new(DownloadStrategy::default()), + download_streaming: AtomicBool::new(true), number_of_open_requests: AtomicUsize::new(0), ping_time_ms: AtomicUsize::new(INITIAL_PING_TIME_ESTIMATE.as_millis() as usize), read_position: AtomicUsize::new(0), @@ -507,24 +495,23 @@ impl Read for AudioFileStreaming { return Ok(0); } - 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. - let ping_time_seconds = - Duration::from_millis(self.shared.ping_time_ms.load(Ordering::Relaxed) as u64) - .as_secs_f32(); + let length_to_request = if self.shared.download_streaming.load(Ordering::Acquire) { + // Due to the read-ahead stuff, we potentially request more than the actual request demanded. + let ping_time_seconds = + Duration::from_millis(self.shared.ping_time_ms.load(Ordering::Relaxed) as u64) + .as_secs_f32(); - let length_to_request = length - + max( - (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() - * self.shared.bytes_per_second as f32) as usize, - (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS - * ping_time_seconds - * self.shared.bytes_per_second as f32) as usize, - ); - min(length_to_request, self.shared.file_size - offset) - } + let length_to_request = length + + max( + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * self.shared.bytes_per_second as f32) + as usize, + (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS + * ping_time_seconds + * self.shared.bytes_per_second as f32) as usize, + ); + min(length_to_request, self.shared.file_size - offset) + } else { + length }; let mut ranges_to_request = RangeSet::new(); @@ -575,7 +562,7 @@ impl Read for AudioFileStreaming { impl Seek for AudioFileStreaming { fn seek(&mut self, pos: SeekFrom) -> io::Result { - // If we are already at this position, we don't need to switch download strategy. + // If we are already at this position, we don't need to switch download mode. // These checks and locks are less expensive than interrupting streaming. let current_position = self.position as i64; let requested_pos = match pos { @@ -595,13 +582,17 @@ impl Seek for AudioFileStreaming { .downloaded .contains(requested_pos as usize); - let mut old_strategy = DownloadStrategy::default(); + let mut was_streaming = false; if !available { // Ensure random access mode if we need to download this part. - old_strategy = std::mem::replace( - &mut *(self.shared.download_strategy.lock()), - DownloadStrategy::RandomAccess(), - ); + // Checking whether we are streaming now is a micro-optimization + // to save an atomic load. + was_streaming = self.shared.download_streaming.load(Ordering::Acquire); + if was_streaming { + self.shared + .download_streaming + .store(false, Ordering::Release); + } } self.position = self.read_file.seek(pos)?; @@ -609,8 +600,10 @@ impl Seek for AudioFileStreaming { .read_position .store(self.position as usize, Ordering::Release); - if !available && old_strategy != DownloadStrategy::RandomAccess() { - *(self.shared.download_strategy.lock()) = old_strategy; + if !available && was_streaming { + self.shared + .download_streaming + .store(true, Ordering::Release); } Ok(self.position) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 08013b5b..274f0c89 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -16,9 +16,9 @@ use librespot_core::{session::Session, Error}; use crate::range_set::{Range, RangeSet}; use super::{ - AudioFileError, AudioFileResult, AudioFileShared, DownloadStrategy, StreamLoaderCommand, - StreamingRequest, FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, - MAX_PREFETCH_REQUESTS, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, + AudioFileError, AudioFileResult, AudioFileShared, StreamLoaderCommand, StreamingRequest, + FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, + MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, }; struct PartialFileData { @@ -157,8 +157,8 @@ enum ControlFlow { } impl AudioFileFetch { - fn get_download_strategy(&mut self) -> DownloadStrategy { - *(self.shared.download_strategy.lock()) + fn is_download_streaming(&mut self) -> bool { + self.shared.download_streaming.load(Ordering::Acquire) } fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult { @@ -337,15 +337,17 @@ impl AudioFileFetch { ) -> 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()) = DownloadStrategy::RandomAccess(); - } - StreamLoaderCommand::StreamMode() => { - *(self.shared.download_strategy.lock()) = DownloadStrategy::Streaming(); - } - StreamLoaderCommand::Close() => return Ok(ControlFlow::Break), + StreamLoaderCommand::RandomAccessMode => self + .shared + .download_streaming + .store(false, Ordering::Release), + StreamLoaderCommand::StreamMode => self + .shared + .download_streaming + .store(true, Ordering::Release), + StreamLoaderCommand::Close => return Ok(ControlFlow::Break), } Ok(ControlFlow::Continue) @@ -430,7 +432,7 @@ pub(super) async fn audio_file_fetch( else => (), } - if fetch.get_download_strategy() == DownloadStrategy::Streaming() { + if fetch.is_download_streaming() { let number_of_open_requests = fetch.shared.number_of_open_requests.load(Ordering::SeqCst); if number_of_open_requests < MAX_PREFETCH_REQUESTS { From 89a5133bd70e68b1182592d6e98ff17fc1898fba Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 8 Jan 2022 20:51:51 +0100 Subject: [PATCH 091/147] Upgrade `aes-ctr` to latest `aes` --- Cargo.lock | 50 ++++++++------------------------------ audio/Cargo.toml | 2 +- audio/src/decrypt.rs | 6 ++--- core/Cargo.toml | 2 +- core/src/authentication.rs | 2 +- discovery/Cargo.toml | 2 +- discovery/src/server.rs | 4 +-- 7 files changed, 18 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dddd26a5..b2082f9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,44 +19,14 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.6.0" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ - "aes-soft", - "aesni", - "cipher", -] - -[[package]] -name = "aes-ctr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7729c3cde54d67063be556aeac75a81330d802f0259500ca40cb52967f975763" -dependencies = [ - "aes-soft", - "aesni", + "cfg-if 1.0.0", "cipher", + "cpufeatures", "ctr", -] - -[[package]] -name = "aes-soft" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" -dependencies = [ - "cipher", - "opaque-debug", -] - -[[package]] -name = "aesni" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" -dependencies = [ - "cipher", "opaque-debug", ] @@ -261,9 +231,9 @@ dependencies = [ [[package]] name = "cipher" -version = "0.2.5" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ "generic-array", ] @@ -380,9 +350,9 @@ dependencies = [ [[package]] name = "ctr" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" dependencies = [ "cipher", ] @@ -1269,7 +1239,7 @@ dependencies = [ name = "librespot-audio" version = "0.3.1" dependencies = [ - "aes-ctr", + "aes", "byteorder", "bytes", "futures-core", @@ -1357,7 +1327,7 @@ dependencies = [ name = "librespot-discovery" version = "0.3.1" dependencies = [ - "aes-ctr", + "aes", "base64", "cfg-if 1.0.0", "dns-sd", diff --git a/audio/Cargo.toml b/audio/Cargo.toml index c7cf0d7b..3f220929 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -11,7 +11,7 @@ path = "../core" version = "0.3.1" [dependencies] -aes-ctr = "0.6" +aes = { version = "0.7", features = ["ctr"] } byteorder = "1.4" bytes = "1.0" futures-core = { version = "0.3", default-features = false } diff --git a/audio/src/decrypt.rs b/audio/src/decrypt.rs index e11241a9..912d1793 100644 --- a/audio/src/decrypt.rs +++ b/audio/src/decrypt.rs @@ -1,9 +1,7 @@ use std::io; -use aes_ctr::{ - cipher::{ - generic_array::GenericArray, NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek, - }, +use aes::{ + cipher::{generic_array::GenericArray, NewCipher, StreamCipher, StreamCipherSeek}, Aes128Ctr, }; diff --git a/core/Cargo.toml b/core/Cargo.toml index 271e5896..6822a8ba 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -13,7 +13,7 @@ path = "../protocol" version = "0.3.1" [dependencies] -aes = "0.6" +aes = "0.7" base64 = "0.13" byteorder = "1.4" bytes = "1" diff --git a/core/src/authentication.rs b/core/src/authentication.rs index a4d34e2b..82df5060 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -1,6 +1,6 @@ use std::io::{self, Read}; -use aes::Aes192; +use aes::{Aes192, BlockDecrypt}; use byteorder::{BigEndian, ByteOrder}; use hmac::Hmac; use pbkdf2::pbkdf2; diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 0225ab68..373c4c76 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/librespot-org/librespot" edition = "2018" [dependencies] -aes-ctr = "0.6" +aes = { version = "0.7", features = ["ctr"] } base64 = "0.13" cfg-if = "1.0" dns-sd = { version = "0.1.3", optional = true } diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 9cf6837b..f3383228 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -8,9 +8,9 @@ use std::{ task::{Context, Poll}, }; -use aes_ctr::{ +use aes::{ cipher::generic_array::GenericArray, - cipher::{NewStreamCipher, SyncStreamCipher}, + cipher::{NewCipher, StreamCipher}, Aes128Ctr, }; use futures_core::Stream; From 5cc3040bd829b701f84bb521ba297c93567ffa2d Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 8 Jan 2022 21:21:31 +0100 Subject: [PATCH 092/147] Update `futures` --- Cargo.lock | 55 +++++++++++++++------------------------------ audio/Cargo.toml | 7 +++--- connect/Cargo.toml | 2 +- core/Cargo.toml | 4 ++-- metadata/Cargo.toml | 2 +- playback/Cargo.toml | 3 +-- protocol/Cargo.toml | 2 +- 7 files changed, 27 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2082f9d..1b61cf71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,9 +474,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" dependencies = [ "futures-channel", "futures-core", @@ -489,9 +489,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" dependencies = [ "futures-core", "futures-sink", @@ -499,15 +499,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" [[package]] name = "futures-executor" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" dependencies = [ "futures-core", "futures-task", @@ -516,18 +516,16 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" [[package]] name = "futures-macro" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" dependencies = [ - "autocfg", - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -535,23 +533,22 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" +checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" [[package]] name = "futures-task" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" [[package]] name = "futures-util" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" dependencies = [ - "autocfg", "futures-channel", "futures-core", "futures-io", @@ -561,8 +558,6 @@ dependencies = [ "memchr", "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] @@ -1243,7 +1238,6 @@ dependencies = [ "byteorder", "bytes", "futures-core", - "futures-executor", "futures-util", "hyper", "librespot-core", @@ -1371,7 +1365,6 @@ dependencies = [ "alsa", "byteorder", "cpal", - "futures-executor", "futures-util", "glib", "gstreamer", @@ -1988,18 +1981,6 @@ 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 3f220929..3df80c77 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -13,10 +13,9 @@ version = "0.3.1" [dependencies] aes = { version = "0.7", features = ["ctr"] } byteorder = "1.4" -bytes = "1.0" -futures-core = { version = "0.3", default-features = false } -futures-executor = "0.3" -futures-util = { version = "0.3", default_features = false } +bytes = "1" +futures-core = "0.3" +futures-util = "0.3" hyper = { version = "0.14", features = ["client"] } log = "0.4" parking_lot = { version = "0.11", features = ["deadlock_detection"] } diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 37521df9..5a7edb27 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" [dependencies] form_urlencoded = "1.0" -futures-util = { version = "0.3.5", default_features = false } +futures-util = "0.3" log = "0.4" protobuf = "2.14.0" rand = "0.8" diff --git a/core/Cargo.toml b/core/Cargo.toml index 6822a8ba..afbbdb98 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,8 +20,8 @@ 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"] } +futures-core = "0.3" +futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] } hmac = "0.11" httparse = "1.3" http = "0.2" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index c9f108d6..1dd2c702 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -10,7 +10,7 @@ edition = "2018" [dependencies] async-trait = "0.1" byteorder = "1.3" -bytes = "1.0" +bytes = "1" chrono = "0.4" log = "0.4" protobuf = "2.14.0" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 262312c0..514d4425 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -19,8 +19,7 @@ version = "0.3.1" [dependencies] byteorder = "1.4" -futures-executor = "0.3" -futures-util = { version = "0.3", default_features = false, features = ["alloc"] } +futures-util = "0.3" log = "0.4" parking_lot = { version = "0.11", features = ["deadlock_detection"] } shell-words = "1.0.0" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 5e24f288..a67a1604 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -12,5 +12,5 @@ edition = "2018" protobuf = "2.25" [build-dependencies] -protobuf-codegen-pure = "2.25" glob = "0.3.0" +protobuf-codegen-pure = "2.25" From f202f364c99af31e7e929a853d9713ec927e38e5 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 8 Jan 2022 21:27:45 +0100 Subject: [PATCH 093/147] Update `tempfile` --- Cargo.lock | 15 ++++++++++++--- audio/Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b61cf71..15a0c562 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -450,6 +450,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fastrand" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" +dependencies = [ + "instant", +] + [[package]] name = "fixedbitset" version = "0.2.0" @@ -2555,13 +2564,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ "cfg-if 1.0.0", + "fastrand", "libc", - "rand", "redox_syscall", "remove_dir_all", "winapi", diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 3df80c77..bd5eb455 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -19,6 +19,6 @@ futures-util = "0.3" hyper = { version = "0.14", features = ["client"] } log = "0.4" parking_lot = { version = "0.11", features = ["deadlock_detection"] } -tempfile = "3.1" +tempfile = "3" thiserror = "1.0" tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } From 5a8bd5703c531f4222e41c0935c7698e51f25f17 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 8 Jan 2022 23:28:46 +0100 Subject: [PATCH 094/147] Update `tokio` and `hyper-rustls` --- Cargo.lock | 171 ++++++++++++++++++++++++---------------- connect/Cargo.toml | 4 +- core/Cargo.toml | 12 ++- core/src/http_client.rs | 40 +++------- discovery/Cargo.toml | 4 +- 5 files changed, 124 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15a0c562..aaba688b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,7 +345,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" dependencies = [ - "sct", + "sct 0.6.1", ] [[package]] @@ -936,12 +936,12 @@ dependencies = [ "headers", "http", "hyper", - "hyper-rustls", - "rustls-native-certs", + "hyper-rustls 0.22.1", + "rustls-native-certs 0.5.0", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", "tower-service", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -954,11 +954,26 @@ dependencies = [ "futures-util", "hyper", "log", - "rustls", - "rustls-native-certs", + "rustls 0.19.1", + "rustls-native-certs 0.5.0", "tokio", - "tokio-rustls", - "webpki", + "tokio-rustls 0.22.0", + "webpki 0.21.4", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +dependencies = [ + "http", + "hyper", + "log", + "rustls 0.20.2", + "rustls-native-certs 0.6.1", + "tokio", + "tokio-rustls 0.23.2", ] [[package]] @@ -1009,15 +1024,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "input_buffer" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" -dependencies = [ - "bytes", -] - [[package]] name = "instant" version = "0.1.12" @@ -1295,7 +1301,7 @@ dependencies = [ "httparse", "hyper", "hyper-proxy", - "hyper-rustls", + "hyper-rustls 0.23.0", "librespot-protocol", "log", "num", @@ -1310,8 +1316,6 @@ dependencies = [ "protobuf", "quick-xml", "rand", - "rustls", - "rustls-native-certs", "serde", "serde_json", "sha-1", @@ -1866,26 +1870,6 @@ dependencies = [ "indexmap", ] -[[package]] -name = "pin-project" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "pin-project-lite" version = "0.2.7" @@ -2192,8 +2176,20 @@ dependencies = [ "base64", "log", "ring", - "sct", - "webpki", + "sct 0.6.1", + "webpki 0.21.4", +] + +[[package]] +name = "rustls" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" +dependencies = [ + "log", + "ring", + "sct 0.7.0", + "webpki 0.22.0", ] [[package]] @@ -2203,11 +2199,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" dependencies = [ "openssl-probe", - "rustls", + "rustls 0.19.1", "schannel", "security-framework", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca9ebdfa27d3fc180e42879037b5338ab1c040c06affd00d8338598e7800943" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64", +] + [[package]] name = "ryu" version = "1.0.6" @@ -2249,6 +2266,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sdl2" version = "0.34.5" @@ -2643,11 +2670,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" dependencies = [ - "autocfg", "bytes", "libc", "memchr", @@ -2663,9 +2689,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", @@ -2678,9 +2704,20 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ - "rustls", + "rustls 0.19.1", "tokio", - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" +dependencies = [ + "rustls 0.20.2", + "tokio", + "webpki 0.22.0", ] [[package]] @@ -2696,19 +2733,18 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.14.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e96bb520beab540ab664bd5a9cfeaa1fcd846fa68c830b42e2c8963071251d2" +checksum = "e80b39df6afcc12cdf752398ade96a6b9e99c903dfdc36e53ad10b9c366bca72" dependencies = [ "futures-util", "log", - "pin-project", - "rustls", + "rustls 0.20.2", + "rustls-native-certs 0.6.1", "tokio", - "tokio-rustls", + "tokio-rustls 0.23.2", "tungstenite", - "webpki", - "webpki-roots", + "webpki 0.22.0", ] [[package]] @@ -2768,25 +2804,23 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "tungstenite" -version = "0.13.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8dada8c1a3aeca77d6b51a4f1314e0f4b8e438b7b1b71e3ddaca8080e4093" +checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1" dependencies = [ "base64", "byteorder", "bytes", "http", "httparse", - "input_buffer", "log", "rand", - "rustls", + "rustls 0.20.2", "sha-1", "thiserror", "url", "utf-8", - "webpki", - "webpki-roots", + "webpki 0.22.0", ] [[package]] @@ -2986,12 +3020,13 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.21.1" +name = "webpki" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" dependencies = [ - "webpki", + "ring", + "untrusted", ] [[package]] diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 5a7edb27..a7340ffd 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -16,8 +16,8 @@ rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.0", features = ["macros", "parking_lot", "sync"] } -tokio-stream = "0.1.1" +tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } +tokio-stream = "0.1" [dependencies.librespot-core] path = "../core" diff --git a/core/Cargo.toml b/core/Cargo.toml index afbbdb98..2eec7365 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -26,8 +26,8 @@ hmac = "0.11" httparse = "1.3" http = "0.2" 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"] } +hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] } +hyper-rustls = { version = "0.23", features = ["http2"] } log = "0.4" num = "0.4" num-bigint = { version = "0.4", features = ["rand"] } @@ -41,16 +41,14 @@ 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" -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 = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } +tokio-stream = "0.1" +tokio-tungstenite = { version = "*", default-features = false, features = ["rustls-tls-native-roots"] } tokio-util = { version = "0.6", features = ["codec"] } url = "2.1" uuid = { version = "0.8", default-features = false, features = ["v4"] } diff --git a/core/src/http_client.rs b/core/src/http_client.rs index e445b953..7a642444 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -9,8 +9,7 @@ use hyper::{ Body, Client, Request, Response, StatusCode, }; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; -use hyper_rustls::HttpsConnector; -use rustls::{ClientConfig, RootCertStore}; +use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; use thiserror::Error; use url::Url; @@ -71,10 +70,11 @@ impl From for Error { } } +#[derive(Clone)] pub struct HttpClient { user_agent: HeaderValue, proxy: Option, - tls_config: ClientConfig, + https_connector: HttpsConnector, } impl HttpClient { @@ -99,32 +99,21 @@ impl HttpClient { let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| { error!("Invalid user agent <{}>: {}", user_agent_str, err); - error!("Please report this as a bug."); HeaderValue::from_static(FALLBACK_USER_AGENT) }); // 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)) => { - 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(); - tls_config.root_store = root_store; - tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + let https_connector = HttpsConnectorBuilder::new() + .with_native_roots() + .https_or_http() + .enable_http1() + .enable_http2() + .build(); Self { user_agent, proxy: proxy.cloned(), - tls_config, + https_connector, } } @@ -154,24 +143,19 @@ impl HttpClient { } pub fn request_fut(&self, mut req: Request) -> Result { - let mut http = HttpConnector::new(); - http.enforce_http(false); - - 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()); 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(https_connector, proxy)?; + let proxy_connector = ProxyConnector::from_proxy(self.https_connector.clone(), proxy)?; Client::builder().build(proxy_connector).request(req) } else { Client::builder() .http2_adaptive_window(true) - .build(https_connector) + .build(self.https_connector.clone()) .request(req) }; diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 373c4c76..2c021bed 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -23,7 +23,7 @@ rand = "0.8" serde_json = "1.0.25" sha-1 = "0.9" thiserror = "1.0" -tokio = { version = "1.0", features = ["parking_lot", "sync", "rt"] } +tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } [dependencies.librespot-core] path = "../core" @@ -32,7 +32,7 @@ version = "0.3.1" [dev-dependencies] futures = "0.3" hex = "0.4" -tokio = { version = "1.0", features = ["macros", "parking_lot", "rt"] } +tokio = { version = "1", features = ["macros", "parking_lot", "rt"] } [features] with-dns-sd = ["dns-sd", "librespot-core/with-dns-sd"] From 56f3c39fc68d82903d3576c8d7719e84e0172fbf Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 9 Jan 2022 00:25:47 +0100 Subject: [PATCH 095/147] Update `hmac`, `pbkdf2`, `serde`, `serde_json`, `sha-1` --- Cargo.lock | 90 ++++++++++++++++++++++---------- connect/Cargo.toml | 2 +- core/Cargo.toml | 12 ++--- core/src/connection/handshake.rs | 2 +- discovery/Cargo.toml | 4 +- discovery/src/server.rs | 4 +- metadata/Cargo.toml | 2 +- protocol/Cargo.toml | 4 +- 8 files changed, 78 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aaba688b..50301b11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,6 +156,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.8.0" @@ -330,13 +339,12 @@ dependencies = [ ] [[package]] -name = "crypto-mac" -version = "0.11.1" +name = "crypto-common" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" dependencies = [ "generic-array", - "subtle", ] [[package]] @@ -412,6 +420,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +dependencies = [ + "block-buffer 0.10.0", + "crypto-common", + "generic-array", + "subtle", +] + [[package]] name = "dns-sd" version = "0.1.3" @@ -804,7 +824,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1", + "sha-1 0.9.8", ] [[package]] @@ -842,12 +862,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +checksum = "ddca131f3e7f2ce2df364b57949a9d47915cfbd35e46cfee355ccebbf794d6a2" dependencies = [ - "crypto-mac", - "digest", + "digest 0.10.1", ] [[package]] @@ -869,7 +888,7 @@ checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 0.4.8", ] [[package]] @@ -916,7 +935,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 0.4.8", "pin-project-lite", "socket2", "tokio", @@ -1048,6 +1067,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + [[package]] name = "jack" version = "0.7.3" @@ -1239,7 +1264,7 @@ dependencies = [ "librespot-protocol", "log", "rpassword", - "sha-1", + "sha-1 0.9.8", "thiserror", "tokio", "url", @@ -1318,7 +1343,7 @@ dependencies = [ "rand", "serde", "serde_json", - "sha-1", + "sha-1 0.10.0", "shannon", "thiserror", "tokio", @@ -1350,7 +1375,7 @@ dependencies = [ "log", "rand", "serde_json", - "sha-1", + "sha-1 0.10.0", "thiserror", "tokio", ] @@ -1840,11 +1865,11 @@ checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" [[package]] name = "pbkdf2" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +checksum = "a4628cc3cf953b82edcd3c1388c5715401420ce5524fedbab426bd5aba017434" dependencies = [ - "crypto-mac", + "digest 0.10.1", "hmac", ] @@ -2330,18 +2355,18 @@ checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.130" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" dependencies = [ "proc-macro2", "quote", @@ -2350,11 +2375,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.72" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" +checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" dependencies = [ - "itoa", + "itoa 1.0.1", "ryu", "serde", ] @@ -2365,13 +2390,24 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.1", +] + [[package]] name = "shannon" version = "0.2.0" @@ -2816,7 +2852,7 @@ dependencies = [ "log", "rand", "rustls 0.20.2", - "sha-1", + "sha-1 0.9.8", "thiserror", "url", "utf-8", diff --git a/connect/Cargo.toml b/connect/Cargo.toml index a7340ffd..3408043b 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" form_urlencoded = "1.0" futures-util = "0.3" log = "0.4" -protobuf = "2.14.0" +protobuf = "2" rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index 2eec7365..bef3dd25 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -18,11 +18,11 @@ base64 = "0.13" byteorder = "1.4" bytes = "1" chrono = "0.4" -dns-sd = { version = "0.1.3", optional = true } +dns-sd = { version = "0.1", optional = true } form_urlencoded = "1.0" futures-core = "0.3" futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] } -hmac = "0.11" +hmac = "0.12" httparse = "1.3" http = "0.2" hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] } @@ -36,15 +36,15 @@ 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"] } +pbkdf2 = { version = "0.10", default-features = false, features = ["hmac"] } priority-queue = "1.1" -protobuf = "2.14.0" +protobuf = "2" quick-xml = { version = "0.22", features = ["serialize"] } rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -sha-1 = "0.9" -shannon = "0.2.0" +sha-1 = "0.10" +shannon = "0.2" thiserror = "1.0" tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } tokio-stream = "0.1" diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 42d64df2..e686c774 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -1,7 +1,7 @@ use std::{env::consts::ARCH, io}; use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; -use hmac::{Hmac, Mac, NewMac}; +use hmac::{Hmac, Mac}; use protobuf::{self, Message}; use rand::{thread_rng, RngCore}; use sha1::Sha1; diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 2c021bed..f329ae98 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -15,13 +15,13 @@ dns-sd = { version = "0.1.3", optional = true } form_urlencoded = "1.0" futures-core = "0.3" futures-util = "0.3" -hmac = "0.11" +hmac = "0.12" 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" +sha-1 = "0.10" thiserror = "1.0" tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } diff --git a/discovery/src/server.rs b/discovery/src/server.rs index f3383228..6c63e683 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -15,7 +15,7 @@ use aes::{ }; use futures_core::Stream; use futures_util::{FutureExt, TryFutureExt}; -use hmac::{Hmac, Mac, NewMac}; +use hmac::{Hmac, Mac}; use hyper::{ service::{make_service_fn, service_fn}, Body, Method, Request, Response, StatusCode, @@ -137,7 +137,7 @@ impl RequestHandler { 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() { + if h.verify_slice(cksum).is_err() { warn!("Login error for user {:?}: MAC mismatch", username); let result = json!({ "status": 102, diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 1dd2c702..46e7af48 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -13,7 +13,7 @@ byteorder = "1.3" bytes = "1" chrono = "0.4" log = "0.4" -protobuf = "2.14.0" +protobuf = "2" thiserror = "1" uuid = { version = "0.8", default-features = false } diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index a67a1604..a7663695 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.25" +protobuf = "2" [build-dependencies] glob = "0.3.0" -protobuf-codegen-pure = "2.25" +protobuf-codegen-pure = "2" From fbff879f3d82a13c963e0803c778e5397e7aa755 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 9 Jan 2022 01:03:47 +0100 Subject: [PATCH 096/147] Update `http`, `once_cell`, `vergen` --- Cargo.lock | 137 +++++++++++++++++++++++++++++++++++--------- core/Cargo.toml | 14 ++--- core/build.rs | 18 ++++-- core/src/version.rs | 6 +- 4 files changed, 132 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50301b11..1302d105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,11 +457,43 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "enum-iterator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" dependencies = [ "atty", "humantime", @@ -620,12 +652,37 @@ dependencies = [ "wasi", ] +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "gimli" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" +[[package]] +name = "git2" +version = "0.13.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "glib" version = "0.10.3" @@ -882,13 +939,13 @@ dependencies = [ [[package]] name = "http" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ "bytes", "fnv", - "itoa 0.4.8", + "itoa 1.0.1", ] [[package]] @@ -1153,6 +1210,18 @@ version = "0.2.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98a04dce437184842841303488f70d0188c5f51437d2a834dc097eafa909a01" +[[package]] +name = "libgit2-sys" +version = "0.12.26+1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.6.7" @@ -1250,7 +1319,7 @@ name = "librespot" version = "0.3.1" dependencies = [ "base64", - "env_logger", + "env_logger 0.8.4", "futures-util", "getopts", "hex", @@ -1317,7 +1386,7 @@ dependencies = [ "bytes", "chrono", "dns-sd", - "env_logger", + "env_logger 0.9.0", "form_urlencoded", "futures-core", "futures-util", @@ -1437,6 +1506,18 @@ dependencies = [ "protobuf-codegen-pure", ] +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.5" @@ -1813,9 +1894,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "opaque-debug" @@ -2183,15 +2264,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - [[package]] name = "rustls" version = "0.19.1" @@ -2250,6 +2322,12 @@ dependencies = [ "base64", ] +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + [[package]] name = "ryu" version = "1.0.6" @@ -2347,12 +2425,6 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" - [[package]] name = "serde" version = "1.0.133" @@ -2932,14 +3004,25 @@ dependencies = [ ] [[package]] -name = "vergen" -version = "3.2.0" +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7141e445af09c8919f1d5f8a20dae0b20c3b57a45dee0d5823c6ed5d237f15a" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0c9f8387e118573859ae0e6c6fbdfa41bd1f4fbea451b0b8c5a81a3b8bc9e0" dependencies = [ - "bitflags", + "anyhow", + "cfg-if 1.0.0", "chrono", - "rustc_version", + "enum-iterator", + "getset", + "git2", + "rustversion", + "thiserror", ] [[package]] diff --git a/core/Cargo.toml b/core/Cargo.toml index bef3dd25..8ae38091 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,7 +23,7 @@ form_urlencoded = "1.0" futures-core = "0.3" futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] } hmac = "0.12" -httparse = "1.3" +httparse = "1.5" http = "0.2" hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] } hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] } @@ -34,10 +34,10 @@ num-bigint = { version = "0.4", features = ["rand"] } num-derive = "0.3" num-integer = "0.1" num-traits = "0.2" -once_cell = "1.5.2" +once_cell = "1.9" parking_lot = { version = "0.11", features = ["deadlock_detection"] } pbkdf2 = { version = "0.10", default-features = false, features = ["hmac"] } -priority-queue = "1.1" +priority-queue = "1.2.1" protobuf = "2" quick-xml = { version = "0.22", features = ["serialize"] } rand = "0.8" @@ -50,16 +50,16 @@ tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", tokio-stream = "0.1" tokio-tungstenite = { version = "*", default-features = false, features = ["rustls-tls-native-roots"] } tokio-util = { version = "0.6", features = ["codec"] } -url = "2.1" +url = "2" uuid = { version = "0.8", default-features = false, features = ["v4"] } [build-dependencies] rand = "0.8" -vergen = "3.0.4" +vergen = { version = "6", default-features = false, features = ["build", "git"] } [dev-dependencies] -env_logger = "0.8" -tokio = { version = "1.0", features = ["macros", "parking_lot"] } +env_logger = "0.9" +tokio = { version = "1", features = ["macros", "parking_lot"] } [features] with-dns-sd = ["dns-sd"] diff --git a/core/build.rs b/core/build.rs index 8e61c912..85a626e9 100644 --- a/core/build.rs +++ b/core/build.rs @@ -1,11 +1,17 @@ -use rand::distributions::Alphanumeric; -use rand::Rng; -use vergen::{generate_cargo_keys, ConstantsFlags}; +use rand::{distributions::Alphanumeric, Rng}; +use vergen::{vergen, Config, ShaKind, TimestampKind}; fn main() { - let mut flags = ConstantsFlags::all(); - flags.toggle(ConstantsFlags::REBUILD_ON_HEAD_CHANGE); - generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!"); + let mut config = Config::default(); + *config.build_mut().kind_mut() = TimestampKind::DateOnly; + *config.git_mut().enabled_mut() = true; + *config.git_mut().commit_timestamp_mut() = true; + *config.git_mut().commit_timestamp_kind_mut() = TimestampKind::DateOnly; + *config.git_mut().sha_mut() = true; + *config.git_mut().sha_kind_mut() = ShaKind::Short; + *config.git_mut().rerun_on_head_change_mut() = true; + + vergen(config).expect("Unable to generate the cargo keys!"); let build_id: String = rand::thread_rng() .sample_iter(Alphanumeric) diff --git a/core/src/version.rs b/core/src/version.rs index 98047ef1..aadc1356 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -1,14 +1,14 @@ /// Version string of the form "librespot-" -pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_SHA_SHORT")); +pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_GIT_SHA_SHORT")); /// Generate a timestamp string representing the build date (UTC). pub const BUILD_DATE: &str = env!("VERGEN_BUILD_DATE"); /// Short sha of the latest git commit. -pub const SHA_SHORT: &str = env!("VERGEN_SHA_SHORT"); +pub const SHA_SHORT: &str = env!("VERGEN_GIT_SHA_SHORT"); /// Date of the latest git commit. -pub const COMMIT_DATE: &str = env!("VERGEN_COMMIT_DATE"); +pub const COMMIT_DATE: &str = env!("VERGEN_GIT_COMMIT_DATE"); /// Librespot crate version. pub const SEMVER: &str = env!("CARGO_PKG_VERSION"); From 59d00787c9efb3039653a85db3bb2a39453c8c98 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 9 Jan 2022 16:04:53 +0100 Subject: [PATCH 097/147] Update player crates and transitive dependencies --- Cargo.lock | 344 +++++++++++++----------- Cargo.toml | 8 +- discovery/Cargo.toml | 2 +- metadata/Cargo.toml | 2 +- playback/Cargo.toml | 16 +- playback/src/audio_backend/alsa.rs | 5 +- playback/src/audio_backend/gstreamer.rs | 14 +- protocol/Cargo.toml | 2 +- src/main.rs | 3 +- src/player_event_handler.rs | 12 +- 10 files changed, 210 insertions(+), 198 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1302d105..307253bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cipher", "cpufeatures", "ctr", @@ -48,7 +48,19 @@ dependencies = [ "alsa-sys", "bitflags", "libc", - "nix", + "nix 0.20.0", +] + +[[package]] +name = "alsa" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5915f52fe2cf65e83924d037b6c5290b7cee097c6b5c8700746e6168a343fd6b" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix 0.23.1", ] [[package]] @@ -63,9 +75,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203" +checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" [[package]] name = "arrayvec" @@ -75,9 +87,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-trait" -version = "0.1.51" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" dependencies = [ "proc-macro2", "quote", @@ -109,7 +121,7 @@ checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" dependencies = [ "addr2line", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", "miniz_oxide", "object", @@ -167,9 +179,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.8.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "bytemuck" @@ -214,10 +226,13 @@ dependencies = [ ] [[package]] -name = "cfg-if" -version = "0.1.10" +name = "cfg-expr" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "b412e83326147c2bb881f8b40edfbf9905b9b8abaebd0e47ca190ba62fda8f0e" +dependencies = [ + "smallvec", +] [[package]] name = "cfg-if" @@ -309,10 +324,10 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" dependencies = [ - "alsa", + "alsa 0.5.0", "core-foundation-sys", "coreaudio-rs", - "jack", + "jack 0.7.3", "jni", "js-sys", "lazy_static", @@ -320,7 +335,7 @@ dependencies = [ "mach", "ndk 0.3.0", "ndk-glue 0.3.0", - "nix", + "nix 0.20.0", "oboe", "parking_lot", "stdweb", @@ -400,17 +415,6 @@ dependencies = [ "syn", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "digest" version = "0.9.0" @@ -454,7 +458,7 @@ version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -477,18 +481,6 @@ dependencies = [ "syn", ] -[[package]] -name = "env_logger" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" -dependencies = [ - "atty", - "humantime", - "log", - "termcolor", -] - [[package]] name = "env_logger" version = "0.9.0" @@ -624,9 +616,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", @@ -647,7 +639,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi", ] @@ -685,33 +677,32 @@ dependencies = [ [[package]] name = "glib" -version = "0.10.3" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" +checksum = "7c515f1e62bf151ef6635f528d05b02c11506de986e43b34a5c920ef0b3796a4" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-executor", "futures-task", - "futures-util", "glib-macros", "glib-sys", "gobject-sys", "libc", "once_cell", + "smallvec", ] [[package]] name = "glib-macros" -version = "0.10.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" +checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" dependencies = [ "anyhow", "heck", - "itertools", - "proc-macro-crate 0.1.5", + "proc-macro-crate 1.1.0", "proc-macro-error", "proc-macro2", "quote", @@ -720,9 +711,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.10.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" +checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" dependencies = [ "libc", "system-deps", @@ -736,9 +727,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "gobject-sys" -version = "0.10.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" +checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" dependencies = [ "glib-sys", "libc", @@ -747,22 +738,21 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.16.7" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33" +checksum = "c6a255f142048ba2c4a4dce39106db1965abe355d23f4b5335edea43a553faa4" dependencies = [ "bitflags", - "cfg-if 1.0.0", + "cfg-if", "futures-channel", "futures-core", "futures-util", "glib", - "glib-sys", - "gobject-sys", "gstreamer-sys", "libc", "muldiv", - "num-rational 0.3.2", + "num-integer", + "num-rational", "once_cell", "paste", "pretty-hex", @@ -771,29 +761,26 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.16.5" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc80888271338c3ede875d8cafc452eb207476ff5539dcbe0018a8f5b827af0e" +checksum = "f73b8d33b1bbe9f22d0cf56661a1d2a2c9a0e099ea10e5f1f347be5038f5c043" dependencies = [ "bitflags", "futures-core", "futures-sink", "glib", - "glib-sys", - "gobject-sys", "gstreamer", "gstreamer-app-sys", "gstreamer-base", - "gstreamer-sys", "libc", "once_cell", ] [[package]] name = "gstreamer-app-sys" -version = "0.9.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "813f64275c9e7b33b828b9efcf9dfa64b95996766d4de996e84363ac65b87e3d" +checksum = "41865cfb8a5ddfa1161734a0d068dcd4689da852be0910b40484206408cfeafa" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -804,25 +791,23 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.16.5" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bafd01c56f59cb10f4b5a10f97bb4bdf8c2b2784ae5b04da7e2d400cf6e6afcf" +checksum = "2c0c1d8c62eb5d08fb80173609f2eea71d385393363146e4e78107facbd67715" dependencies = [ "bitflags", + "cfg-if", "glib", - "glib-sys", - "gobject-sys", "gstreamer", "gstreamer-base-sys", - "gstreamer-sys", "libc", ] [[package]] name = "gstreamer-base-sys" -version = "0.9.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b7b6dc2d6e160a1ae28612f602bd500b3fa474ce90bf6bb2f08072682beef5" +checksum = "28169a7b58edb93ad8ac766f0fa12dcd36a2af4257a97ee10194c7103baf3e27" dependencies = [ "glib-sys", "gobject-sys", @@ -833,9 +818,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.9.1" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1f154082d01af5718c5f8a8eb4f565a4ea5586ad8833a8fc2c2aa6844b601d" +checksum = "a81704feeb3e8599913bdd1e738455c2991a01ff4a1780cb62200993e454cc3e" dependencies = [ "glib-sys", "gobject-sys", @@ -845,9 +830,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f072413d126e57991455e0a922b31e4c8ba7c2ffbebf6b78b4f8521397d65cd" +checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689" dependencies = [ "bytes", "fnv", @@ -1092,9 +1077,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg", "hashbrown", @@ -1106,14 +1091,14 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] name = "itertools" -version = "0.9.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" dependencies = [ "either", ] @@ -1144,14 +1129,28 @@ dependencies = [ ] [[package]] -name = "jack-sys" -version = "0.2.2" +name = "jack" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41" +checksum = "5d2ac12f11bb369f3c50d24dbb9fdb00dc987434c9dd622a12c13f618106e153" +dependencies = [ + "bitflags", + "jack-sys", + "lazy_static", + "libc", + "log", +] + +[[package]] +name = "jack-sys" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b91f2d2d10bc2bab38f4dfa4bc77123a988828af39dd3f30dd9db14d44f2cc1" dependencies = [ "lazy_static", "libc", "libloading 0.6.7", + "pkg-config", ] [[package]] @@ -1206,9 +1205,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.109" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98a04dce437184842841303488f70d0188c5f51437d2a834dc097eafa909a01" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" [[package]] name = "libgit2-sys" @@ -1228,7 +1227,7 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "winapi", ] @@ -1238,7 +1237,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "winapi", ] @@ -1318,12 +1317,10 @@ dependencies = [ name = "librespot" version = "0.3.1" dependencies = [ - "base64", - "env_logger 0.8.4", + "env_logger", "futures-util", "getopts", "hex", - "hyper", "librespot-audio", "librespot-connect", "librespot-core", @@ -1333,7 +1330,7 @@ dependencies = [ "librespot-protocol", "log", "rpassword", - "sha-1 0.9.8", + "sha-1 0.10.0", "thiserror", "tokio", "url", @@ -1386,7 +1383,7 @@ dependencies = [ "bytes", "chrono", "dns-sd", - "env_logger 0.9.0", + "env_logger", "form_urlencoded", "futures-core", "futures-util", @@ -1430,7 +1427,7 @@ version = "0.3.1" dependencies = [ "aes", "base64", - "cfg-if 1.0.0", + "cfg-if", "dns-sd", "form_urlencoded", "futures", @@ -1469,14 +1466,14 @@ dependencies = [ name = "librespot-playback" version = "0.3.1" dependencies = [ - "alsa", + "alsa 0.6.0", "byteorder", "cpal", "futures-util", "glib", "gstreamer", "gstreamer-app", - "jack", + "jack 0.8.4", "libpulse-binding", "libpulse-simple-binding", "librespot-audio", @@ -1533,7 +1530,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1563,6 +1560,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" @@ -1603,9 +1609,9 @@ dependencies = [ [[package]] name = "muldiv" -version = "0.2.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +checksum = "b5136edda114182728ccdedb9f5eda882781f35fa6e80cc360af12a8932507f3" [[package]] name = "multimap" @@ -1696,10 +1702,23 @@ checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" dependencies = [ "bitflags", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", ] +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "5.1.2" @@ -1729,7 +1748,7 @@ dependencies = [ "num-complex", "num-integer", "num-iter", - "num-rational 0.4.0", + "num-rational", "num-traits", ] @@ -1786,17 +1805,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-rational" version = "0.4.0" @@ -1821,9 +1829,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", @@ -1831,19 +1839,18 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" +checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" dependencies = [ - "derivative", "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" +checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" dependencies = [ "proc-macro-crate 1.1.0", "proc-macro2", @@ -1928,7 +1935,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "backtrace", - "cfg-if 1.0.0", + "cfg-if", "instant", "libc", "petgraph", @@ -1978,9 +1985,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -1990,9 +1997,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.23" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a3ea4f0dd7f1f3e512cf97bf100819aa547f36a6eccac8dbaae839eb92363e" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" [[package]] name = "portaudio-rs" @@ -2017,9 +2024,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "pretty-hex" @@ -2082,9 +2089,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ "unicode-xid", ] @@ -2126,9 +2133,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" dependencies = [ "proc-macro2", ] @@ -2330,9 +2337,9 @@ checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "same-file" @@ -2381,9 +2388,9 @@ dependencies = [ [[package]] name = "sdl2" -version = "0.34.5" +version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deecbc3fa9460acff5a1e563e05cb5f31bba0aa0c214bb49a43db8159176d54b" +checksum = "f035f8e87735fa3a8437292be49fe6056450f7cbb13c230b4bcd1bdd7279421f" dependencies = [ "bitflags", "lazy_static", @@ -2393,13 +2400,13 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.34.5" +version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a29aa21f175b5a41a6e26da572d5e5d1ee5660d35f9f9d0913e8a802098f74" +checksum = "94cb479353c0603785c834e2307440d83d196bf255f204f7f6741358de8d6a2f" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "libc", - "version-compare", + "version-compare 0.1.0", ] [[package]] @@ -2463,7 +2470,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.9.0", "opaque-debug", @@ -2475,7 +2482,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cpufeatures", "digest 0.10.1", ] @@ -2552,15 +2559,15 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "strum" -version = "0.18.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" [[package]] name = "strum_macros" -version = "0.18.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" dependencies = [ "heck", "proc-macro2", @@ -2661,9 +2668,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" dependencies = [ "proc-macro2", "quote", @@ -2684,17 +2691,20 @@ dependencies = [ [[package]] name = "system-deps" -version = "1.3.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" +checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" dependencies = [ + "anyhow", + "cfg-expr", "heck", + "itertools", "pkg-config", "strum", "strum_macros", "thiserror", "toml", - "version-compare", + "version-compare 0.0.11", ] [[package]] @@ -2703,7 +2713,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "fastrand", "libc", "redox_syscall", @@ -2890,7 +2900,7 @@ version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "pin-project-lite", "tracing-core", ] @@ -2933,9 +2943,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicode-bidi" @@ -3016,7 +3026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd0c9f8387e118573859ae0e6c6fbdfa41bd1f4fbea451b0b8c5a81a3b8bc9e0" dependencies = [ "anyhow", - "cfg-if 1.0.0", + "cfg-if", "chrono", "enum-iterator", "getset", @@ -3027,15 +3037,21 @@ dependencies = [ [[package]] name = "version-compare" -version = "0.0.10" +version = "0.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + +[[package]] +name = "version-compare" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" @@ -3070,7 +3086,7 @@ version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen-macro", ] @@ -3181,9 +3197,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "zerocopy" -version = "0.3.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6580539ad917b7c026220c4b3f2c08d52ce54d6ce0dc491e66002e35388fab46" +checksum = "332f188cc1bcf1fe1064b8c58d150f497e697f49774aa846f2dc949d9a25f236" dependencies = [ "byteorder", "zerocopy-derive", @@ -3191,9 +3207,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498dbd1fd7beb83c86709ae1c33ca50942889473473d287d56ce4770a18edfb" +checksum = "a0fbc82b82efe24da867ee52e015e58178684bd9dd64c34e66bdf21da2582a9f" dependencies = [ "proc-macro2", "syn", diff --git a/Cargo.toml b/Cargo.toml index 3df50606..3df60b84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,18 +49,16 @@ path = "protocol" version = "0.3.1" [dependencies] -base64 = "0.13" -env_logger = { version = "0.8", default-features = false, features = ["termcolor", "humantime", "atty"] } +env_logger = { version = "0.9", default-features = false, features = ["termcolor", "humantime", "atty"] } futures-util = { version = "0.3", default_features = false } -getopts = "0.2.21" +getopts = "0.2" hex = "0.4" -hyper = "0.14" log = "0.4" rpassword = "5.0" +sha-1 = "0.10" thiserror = "1.0" tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" -sha-1 = "0.9" [features] alsa-backend = ["librespot-playback/alsa-backend"] diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index f329ae98..ccf055d0 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -20,7 +20,7 @@ hyper = { version = "0.14", features = ["http1", "server", "tcp"] } libmdns = "0.6" log = "0.4" rand = "0.8" -serde_json = "1.0.25" +serde_json = "1.0" sha-1 = "0.10" thiserror = "1.0" tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 46e7af48..ee5b18e8 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" [dependencies] async-trait = "0.1" -byteorder = "1.3" +byteorder = "1" bytes = "1" chrono = "0.4" log = "0.4" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 514d4425..bd495175 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -18,25 +18,25 @@ path = "../metadata" version = "0.3.1" [dependencies] -byteorder = "1.4" +byteorder = "1" futures-util = "0.3" log = "0.4" parking_lot = { version = "0.11", features = ["deadlock_detection"] } shell-words = "1.0.0" thiserror = "1.0" tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } -zerocopy = { version = "0.3" } +zerocopy = "0.6" # Backends -alsa = { version = "0.5", optional = true } +alsa = { version = "0.6", optional = true } portaudio-rs = { version = "0.3", optional = true } libpulse-binding = { version = "2", optional = true, default-features = false } libpulse-simple-binding = { version = "2", optional = true, default-features = false } -jack = { version = "0.7", optional = true } -sdl2 = { version = "0.34.3", optional = true } -gstreamer = { version = "0.16", optional = true } -gstreamer-app = { version = "0.16", optional = true } -glib = { version = "0.10", optional = true } +jack = { version = "0.8", optional = true } +sdl2 = { version = "0.35", optional = true } +gstreamer = { version = "0.17", optional = true } +gstreamer-app = { version = "0.17", optional = true } +glib = { version = "0.14", optional = true } # Rodio dependencies rodio = { version = "0.14", optional = true, default-features = false } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 16aa420d..20e73618 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -90,11 +90,8 @@ impl From for Format { F32 => Format::float(), S32 => Format::s32(), S24 => Format::s24(), + S24_3 => Format::s24_3(), S16 => Format::s16(), - #[cfg(target_endian = "little")] - S24_3 => Format::S243LE, - #[cfg(target_endian = "big")] - S24_3 => Format::S243BE, } } } diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 8b957577..f96dc5dd 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -56,17 +56,17 @@ impl Open for GstreamerSink { let pipeline = pipelinee .dynamic_cast::() .expect("couldn't cast pipeline element at runtime!"); - let bus = pipeline.get_bus().expect("couldn't get bus from pipeline"); + let bus = pipeline.bus().expect("couldn't get bus from pipeline"); let mainloop = glib::MainLoop::new(None, false); let appsrce: gst::Element = pipeline - .get_by_name("appsrc0") + .by_name("appsrc0") .expect("couldn't get appsrc from pipeline"); let appsrc: gst_app::AppSrc = appsrce .dynamic_cast::() .expect("couldn't cast AppSrc element at runtime!"); let bufferpool = gst::BufferPool::new(); - let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps"); - let mut conf = bufferpool.get_config(); + let appsrc_caps = appsrc.caps().expect("couldn't get appsrc caps"); + let mut conf = bufferpool.config(); conf.set_params(Some(&appsrc_caps), 4096 * sample_size as u32, 0, 0); bufferpool .set_config(conf) @@ -99,9 +99,9 @@ impl Open for GstreamerSink { gst::MessageView::Error(err) => { println!( "Error from {:?}: {} ({:?})", - err.get_src().map(|s| s.get_path_string()), - err.get_error(), - err.get_debug() + err.src().map(|s| s.path_string()), + err.error(), + err.debug() ); watch_mainloop.quit(); } diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index a7663695..c90c82d2 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -12,5 +12,5 @@ edition = "2018" protobuf = "2" [build-dependencies] -glob = "0.3.0" +glob = "0.3" protobuf-codegen-pure = "2" diff --git a/src/main.rs b/src/main.rs index 2d0337cc..e3cb4e7a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,6 @@ use std::{ }; use futures_util::{future, FutureExt, StreamExt}; -use librespot_playback::player::PlayerEvent; use log::{error, info, trace, warn}; use sha1::{Digest, Sha1}; use thiserror::Error; @@ -30,7 +29,7 @@ use librespot::{ }, dither, mixer::{self, MixerConfig, MixerFn}, - player::{db_to_ratio, ratio_to_db, Player}, + player::{db_to_ratio, ratio_to_db, Player, PlayerEvent}, }, }; diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 4c75128c..d5e4517b 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -1,11 +1,13 @@ -use librespot::playback::player::PlayerEvent; -use librespot::playback::player::SinkStatus; +use std::{ + collections::HashMap, + io, + process::{Command, ExitStatus}, +}; + use log::info; use tokio::process::{Child as AsyncChild, Command as AsyncCommand}; -use std::collections::HashMap; -use std::io; -use std::process::{Command, ExitStatus}; +use librespot::playback::player::{PlayerEvent, SinkStatus}; pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option> { let mut env_vars = HashMap::new(); From d2c377d14b3a7e2e8462925c2fe9f9b45698a9f4 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 9 Jan 2022 16:28:14 +0100 Subject: [PATCH 098/147] Fix GStreamer cleanup on exit --- playback/src/audio_backend/gstreamer.rs | 43 +++++++++++++++++++------ playback/src/audio_backend/mod.rs | 2 ++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index f96dc5dd..63aafbd0 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,17 +1,20 @@ -use super::{Open, Sink, SinkAsBytes, SinkResult}; -use crate::config::AudioFormat; -use crate::convert::Converter; -use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; +use std::{ + ops::Drop, + sync::mpsc::{sync_channel, SyncSender}, + thread, +}; use gstreamer as gst; use gstreamer_app as gst_app; -use gst::prelude::*; +use gst::{prelude::*, State}; use zerocopy::AsBytes; -use std::sync::mpsc::{sync_channel, SyncSender}; -use std::thread; +use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; + +use crate::{ + config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS, SAMPLE_RATE, +}; #[allow(dead_code)] pub struct GstreamerSink { @@ -115,8 +118,8 @@ impl Open for GstreamerSink { }); pipeline - .set_state(gst::State::Playing) - .expect("unable to set the pipeline to the `Playing` state"); + .set_state(State::Ready) + .expect("unable to set the pipeline to the `Ready` state"); Self { tx, @@ -127,9 +130,29 @@ impl Open for GstreamerSink { } impl Sink for GstreamerSink { + fn start(&mut self) -> SinkResult<()> { + self.pipeline + .set_state(State::Playing) + .map_err(|e| SinkError::StateChange(e.to_string()))?; + Ok(()) + } + + fn stop(&mut self) -> SinkResult<()> { + self.pipeline + .set_state(State::Paused) + .map_err(|e| SinkError::StateChange(e.to_string()))?; + Ok(()) + } + sink_as_bytes!(); } +impl Drop for GstreamerSink { + fn drop(&mut self) { + let _ = self.pipeline.set_state(State::Null); + } +} + impl SinkAsBytes for GstreamerSink { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { // Copy expensively (in to_vec()) to avoid thread synchronization diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 66f2ba29..959bf17d 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -13,6 +13,8 @@ pub enum SinkError { OnWrite(String), #[error("Audio Sink Error Invalid Parameters: {0}")] InvalidParams(String), + #[error("Audio Sink Error Changing State: {0}")] + StateChange(String), } pub type SinkResult = Result; From e69d5a8e91f6d4324e4c9bd303e74d7eaaf25b42 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 9 Jan 2022 20:38:54 +0100 Subject: [PATCH 099/147] Fix GStreamer lagging audio on next track Also: remove unnecessary thread and channel --- playback/src/audio_backend/gstreamer.rs | 79 ++++++++++++++----------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 63aafbd0..0b8b63bc 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,13 +1,13 @@ -use std::{ - ops::Drop, - sync::mpsc::{sync_channel, SyncSender}, - thread, -}; +use std::{ops::Drop, thread}; use gstreamer as gst; use gstreamer_app as gst_app; -use gst::{prelude::*, State}; +use gst::{ + event::{FlushStart, FlushStop}, + prelude::*, + State, +}; use zerocopy::AsBytes; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; @@ -18,7 +18,8 @@ use crate::{ #[allow(dead_code)] pub struct GstreamerSink { - tx: SyncSender>, + appsrc: gst_app::AppSrc, + bufferpool: gst::BufferPool, pipeline: gst::Pipeline, format: AudioFormat, } @@ -35,7 +36,7 @@ impl Open for GstreamerSink { _ => format!("{:?}", format), }; let sample_size = format.size(); - let gst_bytes = 2048 * sample_size; + let gst_bytes = NUM_CHANNELS as usize * 1024 * sample_size; #[cfg(target_endian = "little")] const ENDIANNESS: &str = "LE"; @@ -67,38 +68,25 @@ impl Open for GstreamerSink { let appsrc: gst_app::AppSrc = appsrce .dynamic_cast::() .expect("couldn't cast AppSrc element at runtime!"); - let bufferpool = gst::BufferPool::new(); let appsrc_caps = appsrc.caps().expect("couldn't get appsrc caps"); + + let bufferpool = gst::BufferPool::new(); + let mut conf = bufferpool.config(); - conf.set_params(Some(&appsrc_caps), 4096 * sample_size as u32, 0, 0); + conf.set_params(Some(&appsrc_caps), gst_bytes as u32, 0, 0); bufferpool .set_config(conf) .expect("couldn't configure the buffer pool"); - bufferpool - .set_active(true) - .expect("couldn't activate buffer pool"); - - let (tx, rx) = sync_channel::>(64 * sample_size); - thread::spawn(move || { - for data in rx { - let buffer = bufferpool.acquire_buffer(None); - if let Ok(mut buffer) = buffer { - let mutbuf = buffer.make_mut(); - mutbuf.set_size(data.len()); - mutbuf - .copy_from_slice(0, data.as_bytes()) - .expect("Failed to copy from slice"); - let _eat = appsrc.push_buffer(buffer); - } - } - }); thread::spawn(move || { let thread_mainloop = mainloop; let watch_mainloop = thread_mainloop.clone(); bus.add_watch(move |_, msg| { match msg.view() { - gst::MessageView::Eos(..) => watch_mainloop.quit(), + gst::MessageView::Eos(_) => { + println!("gst signaled end of stream"); + watch_mainloop.quit(); + } gst::MessageView::Error(err) => { println!( "Error from {:?}: {} ({:?})", @@ -122,7 +110,8 @@ impl Open for GstreamerSink { .expect("unable to set the pipeline to the `Ready` state"); Self { - tx, + appsrc, + bufferpool, pipeline, format, } @@ -131,6 +120,10 @@ impl Open for GstreamerSink { impl Sink for GstreamerSink { fn start(&mut self) -> SinkResult<()> { + self.appsrc.send_event(FlushStop::new(true)); + self.bufferpool + .set_active(true) + .expect("couldn't activate buffer pool"); self.pipeline .set_state(State::Playing) .map_err(|e| SinkError::StateChange(e.to_string()))?; @@ -138,9 +131,13 @@ impl Sink for GstreamerSink { } fn stop(&mut self) -> SinkResult<()> { + self.appsrc.send_event(FlushStart::new()); self.pipeline .set_state(State::Paused) .map_err(|e| SinkError::StateChange(e.to_string()))?; + self.bufferpool + .set_active(false) + .expect("couldn't deactivate buffer pool"); Ok(()) } @@ -149,16 +146,30 @@ impl Sink for GstreamerSink { impl Drop for GstreamerSink { fn drop(&mut self) { + // Follow the state transitions documented at: + // https://gstreamer.freedesktop.org/documentation/additional/design/states.html?gi-language=c + let _ = self.pipeline.set_state(State::Ready); let _ = self.pipeline.set_state(State::Null); } } impl SinkAsBytes for GstreamerSink { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { - // Copy expensively (in to_vec()) to avoid thread synchronization - self.tx - .send(data.to_vec()) - .expect("tx send failed in write function"); + let mut buffer = self + .bufferpool + .acquire_buffer(None) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + + let mutbuf = buffer.make_mut(); + mutbuf.set_size(data.len()); + mutbuf + .copy_from_slice(0, data.as_bytes()) + .expect("Failed to copy from slice"); + + self.appsrc + .push_buffer(buffer) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + Ok(()) } } From 75e6441db93988b55089fa3d8426f12e099ab3a2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 9 Jan 2022 22:24:34 +0100 Subject: [PATCH 100/147] Downgrade for MSRV 1.53 --- Cargo.lock | 42 +++--------------------------- playback/Cargo.toml | 2 +- playback/src/audio_backend/alsa.rs | 5 +++- 3 files changed, 9 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 307253bf..90d4cfa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,19 +48,7 @@ dependencies = [ "alsa-sys", "bitflags", "libc", - "nix 0.20.0", -] - -[[package]] -name = "alsa" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5915f52fe2cf65e83924d037b6c5290b7cee097c6b5c8700746e6168a343fd6b" -dependencies = [ - "alsa-sys", - "bitflags", - "libc", - "nix 0.23.1", + "nix", ] [[package]] @@ -324,7 +312,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" dependencies = [ - "alsa 0.5.0", + "alsa", "core-foundation-sys", "coreaudio-rs", "jack 0.7.3", @@ -335,7 +323,7 @@ dependencies = [ "mach", "ndk 0.3.0", "ndk-glue 0.3.0", - "nix 0.20.0", + "nix", "oboe", "parking_lot", "stdweb", @@ -1466,7 +1454,7 @@ dependencies = [ name = "librespot-playback" version = "0.3.1" dependencies = [ - "alsa 0.6.0", + "alsa", "byteorder", "cpal", "futures-util", @@ -1560,15 +1548,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.16" @@ -1706,19 +1685,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nix" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" -dependencies = [ - "bitflags", - "cc", - "cfg-if", - "libc", - "memoffset", -] - [[package]] name = "nom" version = "5.1.2" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index bd495175..707d28f9 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -28,7 +28,7 @@ tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sy zerocopy = "0.6" # Backends -alsa = { version = "0.6", optional = true } +alsa = { version = "0.5", optional = true } portaudio-rs = { version = "0.3", optional = true } libpulse-binding = { version = "2", optional = true, default-features = false } libpulse-simple-binding = { version = "2", optional = true, default-features = false } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 20e73618..16aa420d 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -90,8 +90,11 @@ impl From for Format { F32 => Format::float(), S32 => Format::s32(), S24 => Format::s24(), - S24_3 => Format::s24_3(), S16 => Format::s16(), + #[cfg(target_endian = "little")] + S24_3 => Format::S243LE, + #[cfg(target_endian = "big")] + S24_3 => Format::S243BE, } } } From a62c1fea8fc9394f2112f009744dc5644d836744 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 9 Jan 2022 22:46:44 +0100 Subject: [PATCH 101/147] Fix rare panics on out-of-bounds stream position --- playback/src/player.rs | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index cfa4414e..15e950c1 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1115,26 +1115,32 @@ impl Future for PlayerInternal { // Only notify if we're skipped some packets *or* we are behind. // If we're ahead it's probably due to a buffer of the backend // and we're actually in time. + let new_stream_position = Duration::from_millis( + new_stream_position_ms as u64, + ); let notify_about_position = packet_position.skipped || match *reported_nominal_start_time { None => true, Some(reported_nominal_start_time) => { - let lag = (Instant::now() - - reported_nominal_start_time) - .as_millis() - as i64 - - new_stream_position_ms as i64; - lag > Duration::from_secs(1).as_millis() - as i64 + let mut notify = false; + if let Some(lag) = Instant::now() + .checked_duration_since( + reported_nominal_start_time, + ) + { + if let Some(lag) = + lag.checked_sub(new_stream_position) + { + notify = + lag > Duration::from_secs(1) + } + } + notify } }; if notify_about_position { - *reported_nominal_start_time = Some( - Instant::now() - - Duration::from_millis( - new_stream_position_ms as u64, - ), - ); + *reported_nominal_start_time = + Instant::now().checked_sub(new_stream_position); self.send_event(PlayerEvent::Playing { track_id, play_request_id, @@ -1539,9 +1545,8 @@ impl PlayerInternal { duration_ms: loaded_track.duration_ms, bytes_per_second: loaded_track.bytes_per_second, stream_position_ms: loaded_track.stream_position_ms, - reported_nominal_start_time: Some( - Instant::now() - Duration::from_millis(position_ms as u64), - ), + reported_nominal_start_time: Instant::now() + .checked_sub(Duration::from_millis(position_ms as u64)), suggested_to_preload_next_track: false, is_explicit: loaded_track.is_explicit, }; @@ -1873,7 +1878,7 @@ impl PlayerInternal { } = self.state { *reported_nominal_start_time = - Some(Instant::now() - Duration::from_millis(position_ms as u64)); + Instant::now().checked_sub(Duration::from_millis(position_ms as u64)); self.send_event(PlayerEvent::Playing { track_id, play_request_id, From c067c1524fadf55bc710f6908bc1656b64c08efa Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 9 Jan 2022 23:04:14 +0100 Subject: [PATCH 102/147] Only notify when we are >= 1 second ahead --- playback/src/player.rs | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 15e950c1..944009b9 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1107,23 +1107,41 @@ impl Future for PlayerInternal { Ok(result) => { if let Some((ref packet_position, ref packet)) = result { let new_stream_position_ms = packet_position.position_ms; - *stream_position_ms = new_stream_position_ms; + let expected_position_ms = std::mem::replace( + &mut *stream_position_ms, + new_stream_position_ms, + ); if !passthrough { match packet.samples() { Ok(_) => { - // Only notify if we're skipped some packets *or* we are behind. - // If we're ahead it's probably due to a buffer of the backend - // and we're actually in time. let new_stream_position = Duration::from_millis( new_stream_position_ms as u64, ); - let notify_about_position = packet_position.skipped - || match *reported_nominal_start_time { + + let now = Instant::now(); + + // Only notify if we're skipped some packets *or* we are behind. + // If we're ahead it's probably due to a buffer of the backend + // and we're actually in time. + let notify_about_position = + match *reported_nominal_start_time { None => true, Some(reported_nominal_start_time) => { let mut notify = false; - if let Some(lag) = Instant::now() + + if packet_position.skipped { + if let Some(ahead) = new_stream_position + .checked_sub(Duration::from_millis( + expected_position_ms as u64, + )) + { + notify |= + ahead >= Duration::from_secs(1) + } + } + + if let Some(lag) = now .checked_duration_since( reported_nominal_start_time, ) @@ -1131,16 +1149,18 @@ impl Future for PlayerInternal { if let Some(lag) = lag.checked_sub(new_stream_position) { - notify = - lag > Duration::from_secs(1) + notify |= + lag >= Duration::from_secs(1) } } + notify } }; + if notify_about_position { *reported_nominal_start_time = - Instant::now().checked_sub(new_stream_position); + now.checked_sub(new_stream_position); self.send_event(PlayerEvent::Playing { track_id, play_request_id, From 0b7ccc803c90b6fde8278a54aa053bc70938ad0e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 10 Jan 2022 21:19:47 +0100 Subject: [PATCH 103/147] Fix streaming on slow connections --- audio/src/fetch/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 4a7742ec..1e0070d7 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -61,6 +61,10 @@ impl From for Error { /// Note: smaller requests can happen if part of the block is downloaded already. pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128; +/// The minimum network throughput that we expect. Together with the minimum download size, +/// this will determine the time we will wait for a response. +pub const MINIMUM_THROUGHPUT: usize = 8192; + /// 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 @@ -119,7 +123,8 @@ pub const FAST_PREFETCH_THRESHOLD_FACTOR: f32 = 1.5; pub const MAX_PREFETCH_REQUESTS: usize = 4; /// The time we will wait to obtain status updates on downloading. -pub const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(1); +pub const DOWNLOAD_TIMEOUT: Duration = + Duration::from_secs((MINIMUM_DOWNLOAD_SIZE / MINIMUM_THROUGHPUT) as u64); pub enum AudioFile { Cached(fs::File), From 32df4a401d0769486b3b485d980ae44465287f8a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 12 Jan 2022 22:09:57 +0100 Subject: [PATCH 104/147] Add configurable client ID and listen for updates --- connect/src/spirc.rs | 27 ++++++++++----------------- core/src/config.rs | 4 ++++ core/src/session.rs | 13 +++++++++++-- core/src/spclient.rs | 7 ++++++- core/src/token.rs | 6 ++---- src/main.rs | 1 + 6 files changed, 34 insertions(+), 24 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index ef9da811..af8189de 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -710,7 +710,7 @@ 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); + self.session.set_connection_id(&connection_id); } fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) { @@ -754,23 +754,9 @@ impl SpircTask { } 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", - PlayStatus::kPlayStatusPlay => "kPlayStatusPlay", - }; - - debug!( - "{:?} {:?} {} {} {} {}", - update.get_typ(), - update.get_device_state().get_name(), - update.get_ident(), - update.get_seq_nr(), - update.get_state_update_id(), - state_string, - ); + trace!("Received update frame: {:#?}", update,); + // First see if this update was intended for us. let device_id = &self.ident; let ident = update.get_ident(); if ident == device_id @@ -779,6 +765,13 @@ impl SpircTask { return Err(SpircError::Ident(ident.to_string()).into()); } + for entry in update.get_device_state().get_metadata().iter() { + if entry.get_field_type() == "client_id" { + self.session.set_client_id(entry.get_metadata()); + break; + } + } + match update.get_typ() { MessageType::kMessageTypeHello => self.notify(Some(ident)), diff --git a/core/src/config.rs b/core/src/config.rs index 4c1b1dd8..bc4e3037 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -2,8 +2,11 @@ use std::{fmt, path::PathBuf, str::FromStr}; use url::Url; +const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; + #[derive(Clone, Debug)] pub struct SessionConfig { + pub client_id: String, pub device_id: String, pub proxy: Option, pub ap_port: Option, @@ -14,6 +17,7 @@ impl Default for SessionConfig { fn default() -> SessionConfig { let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string(); SessionConfig { + client_id: KEYMASTER_CLIENT_ID.to_owned(), device_id, proxy: None, ap_port: None, diff --git a/core/src/session.rs b/core/src/session.rs index 2b431715..913f5813 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -71,6 +71,7 @@ pub struct UserData { #[derive(Debug, Clone, Default)] struct SessionData { + client_id: String, connection_id: String, time_delta: i64, invalid: bool, @@ -345,12 +346,20 @@ impl Session { &self.config().device_id } + pub fn client_id(&self) -> String { + self.0.data.read().client_id.clone() + } + + pub fn set_client_id(&self, client_id: &str) { + self.0.data.write().client_id = client_id.to_owned(); + } + pub fn connection_id(&self) -> String { self.0.data.read().connection_id.clone() } - pub fn set_connection_id(&self, connection_id: String) { - self.0.data.write().connection_id = connection_id; + pub fn set_connection_id(&self, connection_id: &str) { + self.0.data.write().connection_id = connection_id.to_owned(); } pub fn username(&self) -> String { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 9985041a..1aa0da00 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -333,7 +333,12 @@ impl SpClient { .get_user_attribute(attribute) .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?; - let url = template.replace("{id}", &preview_id.to_base16()); + let mut url = template.replace("{id}", &preview_id.to_base16()); + let separator = match url.find('?') { + Some(_) => "&", + None => "?", + }; + url.push_str(&format!("{}cid={}", separator, self.session().client_id())); self.request_url(url).await } diff --git a/core/src/token.rs b/core/src/token.rs index 0c0b7394..f7c8d350 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -52,8 +52,6 @@ struct TokenData { } impl TokenProvider { - const KEYMASTER_CLIENT_ID: &'static str = "65b708073fc0480ea92a077233ca87bd"; - fn find_token(&self, scopes: Vec<&str>) -> Option { self.lock(|inner| { for i in 0..inner.tokens.len() { @@ -84,8 +82,8 @@ impl TokenProvider { let query_uri = format!( "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}", scopes, - Self::KEYMASTER_CLIENT_ID, - self.session().device_id() + self.session().client_id(), + self.session().device_id(), ); let request = self.session().mercury().get(query_uri)?; let response = request.await?; diff --git a/src/main.rs b/src/main.rs index e3cb4e7a..818a1c0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1274,6 +1274,7 @@ fn get_setup() -> Setup { } }), tmp_dir, + ..SessionConfig::default() }; let player_config = { From 8d8d6d4fd828191916b73cf7f6cf76342eb0905d Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 12 Jan 2022 22:22:44 +0100 Subject: [PATCH 105/147] Fail opening the stream for anything but HTTP 206 --- audio/src/fetch/mod.rs | 6 ++++++ audio/src/fetch/receive.rs | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 1e0070d7..ef19a45a 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -428,6 +428,12 @@ impl AudioFileStreaming { let request_time = Instant::now(); let response = streamer.next().await.ok_or(AudioFileError::NoData)??; + let code = response.status(); + if code != StatusCode::PARTIAL_CONTENT { + debug!("Streamer expected partial content but got: {}", code); + return Err(AudioFileError::StatusCode(code).into()); + } + let header_value = response .headers() .get(CONTENT_RANGE) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 274f0c89..568efafd 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -61,13 +61,12 @@ async fn receive_data( }; 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 body = response.into_body(); let data = match hyper::body::to_bytes(body).await { Ok(bytes) => bytes, Err(e) => break Err(e.into()), From 78216eb6ee6d7972e31a484d56399500bcfab190 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 13 Jan 2022 19:12:48 +0100 Subject: [PATCH 106/147] Prevent seek before offset --- playback/src/player.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 944009b9..23d04c8a 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -2195,19 +2195,20 @@ impl Seek for Subfile { fn seek(&mut self, pos: SeekFrom) -> io::Result { let pos = match pos { SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset), - x => x, + SeekFrom::End(offset) => { + if (self.length as i64 - offset) < self.offset as i64 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "newpos would be < self.offset", + )); + } + pos + } + _ => pos, }; let newpos = self.stream.seek(pos)?; - - if newpos >= self.offset { - Ok(newpos - self.offset) - } else { - Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - "newpos < self.offset", - )) - } + Ok(newpos - self.offset) } } From ab67370dc8ea620895e510078f4dd2913b0598d7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 13 Jan 2022 20:11:03 +0100 Subject: [PATCH 107/147] Improve checking of download chunks --- audio/src/fetch/mod.rs | 5 ++- audio/src/fetch/receive.rs | 81 ++++++++++++-------------------------- 2 files changed, 29 insertions(+), 57 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index ef19a45a..bbf2a974 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -430,7 +430,10 @@ impl AudioFileStreaming { let code = response.status(); if code != StatusCode::PARTIAL_CONTENT { - debug!("Streamer expected partial content but got: {}", code); + debug!( + "Opening audio file expected partial content but got: {}", + code + ); return Err(AudioFileError::StatusCode(code).into()); } diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 568efafd..cc70a4f0 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -36,13 +36,8 @@ async fn receive_data( file_data_tx: mpsc::UnboundedSender, mut request: StreamingRequest, ) -> AudioFileResult { - let requested_offset = request.offset; - let requested_length = request.length; - - let mut data_offset = requested_offset; - let mut request_length = requested_length; - - // TODO : check Content-Length and Content-Range headers + let mut offset = request.offset; + let mut actual_length = 0; let old_number_of_request = shared .number_of_open_requests @@ -56,13 +51,20 @@ async fn receive_data( None => match request.streamer.next().await { Some(Ok(response)) => response, Some(Err(e)) => break Err(e.into()), - None => break Ok(()), + None => { + if actual_length != request.length { + let msg = + format!("did not expect body to contain {} bytes", actual_length,); + break Err(Error::data_loss(msg)); + } + + break Ok(()); + } }, }; let code = response.status(); if code != StatusCode::PARTIAL_CONTENT { - debug!("Streamer expected partial content but got: {}", code); break Err(AudioFileError::StatusCode(code).into()); } @@ -72,6 +74,12 @@ async fn receive_data( Err(e) => break Err(e.into()), }; + let data_size = data.len(); + file_data_tx.send(ReceivedData::Data(PartialFileData { offset, data }))?; + + actual_length += data_size; + offset += data_size; + if measure_ping_time { let mut duration = Instant::now() - request.request_time; if duration > MAXIMUM_ASSUMED_PING_TIME { @@ -80,62 +88,23 @@ async fn receive_data( file_data_tx.send(ReceivedData::ResponseTime(duration))?; measure_ping_time = false; } - - let data_size = data.len(); - - file_data_tx.send(ReceivedData::Data(PartialFileData { - offset: data_offset, - data, - }))?; - data_offset += data_size; - if request_length < data_size { - warn!( - "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 { - request_length -= data_size; - } - - if request_length == 0 { - break Ok(()); - } }; 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(); - } - } - shared .number_of_open_requests .fetch_sub(1, Ordering::SeqCst); - 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) - } + if let Err(e) = result { + error!( + "Streamer error requesting range {} +{}: {:?}", + request.offset, request.length, e + ); + return Err(e); } + + Ok(()) } struct AudioFileFetch { From e627cb4b3599de33cfcafaa8412cb51e42ccc77e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 13 Jan 2022 21:05:17 +0100 Subject: [PATCH 108/147] Fix panic when retrying a track that already failed --- playback/src/player.rs | 61 +++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 23d04c8a..61d68bfc 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -14,7 +14,10 @@ use std::{ }; use byteorder::{LittleEndian, ReadBytesExt}; -use futures_util::{future, stream::futures_unordered::FuturesUnordered, StreamExt, TryFutureExt}; +use futures_util::{ + future, future::FusedFuture, stream::futures_unordered::FuturesUnordered, StreamExt, + TryFutureExt, +}; use parking_lot::Mutex; use symphonia::core::io::MediaSource; use tokio::sync::{mpsc, oneshot}; @@ -499,7 +502,7 @@ enum PlayerPreload { None, Loading { track_id: SpotifyId, - loader: Pin> + Send>>, + loader: Pin> + Send>>, }, Ready { track_id: SpotifyId, @@ -515,7 +518,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, start_playback: bool, - loader: Pin> + Send>>, + loader: Pin> + Send>>, }, Paused { track_id: SpotifyId, @@ -571,6 +574,7 @@ impl PlayerState { matches!(self, Stopped) } + #[allow(dead_code)] fn is_loading(&self) -> bool { use self::PlayerState::*; matches!(self, Loading { .. }) @@ -1026,31 +1030,34 @@ impl Future for PlayerInternal { play_request_id, } = self.state { - match loader.as_mut().poll(cx) { - Poll::Ready(Ok(loaded_track)) => { - self.start_playback( - track_id, - play_request_id, - loaded_track, - start_playback, - ); - if let PlayerState::Loading { .. } = self.state { - error!("The state wasn't changed by start_playback()"); - exit(1); + // The loader may be terminated if we are trying to load the same track + // as before, and that track failed to open before. + if !loader.as_mut().is_terminated() { + match loader.as_mut().poll(cx) { + Poll::Ready(Ok(loaded_track)) => { + self.start_playback( + track_id, + play_request_id, + loaded_track, + start_playback, + ); + if let PlayerState::Loading { .. } = self.state { + error!("The state wasn't changed by start_playback()"); + exit(1); + } } + Poll::Ready(Err(e)) => { + error!( + "Skipping to next track, unable to load track <{:?}>: {:?}", + track_id, e + ); + self.send_event(PlayerEvent::Unavailable { + track_id, + play_request_id, + }) + } + Poll::Pending => (), } - Poll::Ready(Err(e)) => { - error!( - "Skipping to next track, unable to load track <{:?}>: {:?}", - track_id, e - ); - debug_assert!(self.state.is_loading()); - self.send_event(PlayerEvent::Unavailable { - track_id, - play_request_id, - }) - } - Poll::Pending => (), } } @@ -2000,7 +2007,7 @@ impl PlayerInternal { &mut self, spotify_id: SpotifyId, position_ms: u32, - ) -> impl Future> + Send + 'static { + ) -> impl FusedFuture> + Send + 'static { // This method creates a future that returns the loaded stream and associated info. // Ideally all work should be done using asynchronous code. However, seek() on the // audio stream is implemented in a blocking fashion. Thus, we can't turn it into future From 0cc4466245120ace83b8ec4f196600cf59a05209 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 13 Jan 2022 21:15:27 +0100 Subject: [PATCH 109/147] Improve range checks --- audio/src/fetch/mod.rs | 2 +- audio/src/fetch/receive.rs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index bbf2a974..30b8d859 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -558,7 +558,7 @@ impl Read for AudioFileStreaming { let available_length = download_status .downloaded .contained_length_from_value(offset); - assert!(available_length > 0); + drop(download_status); self.position = self.read_file.seek(SeekFrom::Start(offset as u64))?; diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index cc70a4f0..5d193062 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -53,8 +53,7 @@ async fn receive_data( Some(Err(e)) => break Err(e.into()), None => { if actual_length != request.length { - let msg = - format!("did not expect body to contain {} bytes", actual_length,); + let msg = format!("did not expect body to contain {} bytes", actual_length); break Err(Error::data_loss(msg)); } @@ -83,6 +82,11 @@ async fn receive_data( if measure_ping_time { let mut duration = Instant::now() - request.request_time; if duration > MAXIMUM_ASSUMED_PING_TIME { + warn!( + "Ping time {} ms exceeds maximum {}, setting to maximum", + duration.as_millis(), + MAXIMUM_ASSUMED_PING_TIME.as_millis() + ); duration = MAXIMUM_ASSUMED_PING_TIME; } file_data_tx.send(ReceivedData::ResponseTime(duration))?; @@ -135,7 +139,10 @@ impl AudioFileFetch { } if offset + length > self.shared.file_size { - length = self.shared.file_size - offset; + return Err(Error::out_of_range(format!( + "Range {} +{} exceeds file size {}", + offset, length, self.shared.file_size + ))); } let mut ranges_to_request = RangeSet::new(); From 7fe13be564f1382114758f87743d189904885eff Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Jan 2022 23:24:43 +0100 Subject: [PATCH 110/147] Fix audio file streaming --- audio/src/fetch/receive.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 5d193062..5d19722b 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -96,6 +96,16 @@ async fn receive_data( drop(request.streamer); + let bytes_remaining = request.length - actual_length; + if bytes_remaining > 0 { + { + let missing_range = Range::new(offset, bytes_remaining); + let mut download_status = shared.download_status.lock(); + download_status.requested.subtract_range(&missing_range); + shared.cond.notify_all(); + } + } + shared .number_of_open_requests .fetch_sub(1, Ordering::SeqCst); @@ -139,10 +149,7 @@ impl AudioFileFetch { } if offset + length > self.shared.file_size { - return Err(Error::out_of_range(format!( - "Range {} +{} exceeds file size {}", - offset, length, self.shared.file_size - ))); + length = self.shared.file_size - offset; } let mut ranges_to_request = RangeSet::new(); From dbeeb0f991ac1517c2a96f7a011834b7dd97f3e1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Jan 2022 23:28:09 +0100 Subject: [PATCH 111/147] Switch from `chrono` to `time` --- Cargo.lock | 14 ++++++-- core/Cargo.toml | 2 +- core/src/cdn_url.rs | 5 ++- core/src/date.rs | 70 ++++++++++++++++++------------------ metadata/Cargo.toml | 1 - metadata/src/album.rs | 6 ++-- metadata/src/audio/item.rs | 6 ++-- metadata/src/availability.rs | 17 ++++----- metadata/src/episode.rs | 4 +-- metadata/src/sale_period.rs | 19 +++++----- metadata/src/show.rs | 2 +- metadata/src/track.rs | 7 ++-- 12 files changed, 78 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90d4cfa1..98542dcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,7 +237,7 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time", + "time 0.1.43", "winapi", ] @@ -1369,7 +1369,6 @@ dependencies = [ "base64", "byteorder", "bytes", - "chrono", "dns-sd", "env_logger", "form_urlencoded", @@ -1400,6 +1399,7 @@ dependencies = [ "sha-1 0.10.0", "shannon", "thiserror", + "time 0.3.5", "tokio", "tokio-stream", "tokio-tungstenite", @@ -1441,7 +1441,6 @@ dependencies = [ "async-trait", "byteorder", "bytes", - "chrono", "librespot-core", "librespot-protocol", "log", @@ -2737,6 +2736,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" +dependencies = [ + "libc", +] + [[package]] name = "tinyvec" version = "1.5.1" diff --git a/core/Cargo.toml b/core/Cargo.toml index 8ae38091..ab3be7a7 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,7 +17,6 @@ aes = "0.7" base64 = "0.13" byteorder = "1.4" bytes = "1" -chrono = "0.4" dns-sd = { version = "0.1", optional = true } form_urlencoded = "1.0" futures-core = "0.3" @@ -46,6 +45,7 @@ serde_json = "1.0" sha-1 = "0.10" shannon = "0.2" thiserror = "1.0" +time = "0.3" tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } tokio-stream = "0.1" tokio-tungstenite = { version = "*", default-features = false, features = ["rustls-tls-native-roots"] } diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index befdefd6..7257a9a5 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -3,7 +3,6 @@ use std::{ ops::{Deref, DerefMut}, }; -use chrono::Local; use protobuf::Message; use thiserror::Error; use url::Url; @@ -84,9 +83,9 @@ impl CdnUrl { return Err(CdnUrlError::Unresolved.into()); } - let now = Local::now(); + let now = Date::now_utc(); let url = self.urls.iter().find(|url| match url.1 { - Some(expiry) => now < expiry.as_utc(), + Some(expiry) => now < expiry, None => true, }); diff --git a/core/src/date.rs b/core/src/date.rs index fe052299..d7cf09ef 100644 --- a/core/src/date.rs +++ b/core/src/date.rs @@ -1,30 +1,27 @@ -use std::{convert::TryFrom, fmt::Debug, ops::Deref}; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; -use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc}; -use thiserror::Error; +use time::{error::ComponentRange, Date as _Date, OffsetDateTime, PrimitiveDateTime, Time}; use crate::Error; use librespot_protocol as protocol; use protocol::metadata::Date as DateMessage; -#[derive(Debug, Error)] -pub enum DateError { - #[error("item has invalid timestamp {0}")] - Timestamp(i64), -} - -impl From for Error { - fn from(err: DateError) -> Self { - Error::invalid_argument(err) +impl From for Error { + fn from(err: ComponentRange) -> Self { + Error::out_of_range(err) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Date(pub DateTime); +pub struct Date(pub OffsetDateTime); impl Deref for Date { - type Target = DateTime; + type Target = OffsetDateTime; fn deref(&self) -> &Self::Target { &self.0 } @@ -32,42 +29,43 @@ impl Deref for Date { impl Date { pub fn as_timestamp(&self) -> i64 { - self.0.timestamp() + self.0.unix_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(DateError::Timestamp(timestamp).into()) - } + let date_time = OffsetDateTime::from_unix_timestamp(timestamp)?; + Ok(Self(date_time)) } - pub fn as_utc(&self) -> DateTime { + pub fn as_utc(&self) -> OffsetDateTime { self.0 } - pub fn from_utc(date_time: NaiveDateTime) -> Self { - Self(DateTime::::from_utc(date_time, Utc)) + pub fn from_utc(date_time: PrimitiveDateTime) -> Self { + Self(date_time.assume_utc()) + } + + pub fn now_utc() -> Self { + Self(OffsetDateTime::now_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 TryFrom<&DateMessage> for Date { + type Error = crate::Error; + fn try_from(msg: &DateMessage) -> Result { + let date = _Date::from_calendar_date( + msg.get_year(), + (msg.get_month() as u8).try_into()?, + msg.get_day() as u8, + )?; + let time = Time::from_hms(msg.get_hour() as u8, msg.get_minute() as u8, 0)?; + Ok(Self::from_utc(PrimitiveDateTime::new(date, time))) } } -impl From> for Date { - fn from(date: DateTime) -> Self { - Self(date) +impl From for Date { + fn from(datetime: OffsetDateTime) -> Self { + Self(datetime) } } diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index ee5b18e8..76635219 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -11,7 +11,6 @@ edition = "2018" async-trait = "0.1" byteorder = "1" bytes = "1" -chrono = "0.4" log = "0.4" protobuf = "2" thiserror = "1" diff --git a/metadata/src/album.rs b/metadata/src/album.rs index 6e07ed7e..8a372245 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -101,7 +101,7 @@ impl TryFrom<&::Message> for Album { artists: album.get_artist().try_into()?, album_type: album.get_field_type(), label: album.get_label().to_owned(), - date: album.get_date().into(), + date: album.get_date().try_into()?, popularity: album.get_popularity(), genres: album.get_genre().to_vec(), covers: album.get_cover().into(), @@ -111,12 +111,12 @@ impl TryFrom<&::Message> for Album { copyrights: album.get_copyright().into(), restrictions: album.get_restriction().into(), related: album.get_related().try_into()?, - sale_periods: album.get_sale_period().into(), + sale_periods: album.get_sale_period().try_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(), + availability: album.get_availability().try_into()?, }) } } diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index 89860c04..d2304810 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -1,7 +1,5 @@ use std::fmt::Debug; -use chrono::Local; - use crate::{ availability::{AudioItemAvailability, Availabilities, UnavailabilityReason}, episode::Episode, @@ -12,7 +10,7 @@ use crate::{ use super::file::AudioFiles; -use librespot_core::{session::UserData, spotify_id::SpotifyItemType, Error, Session, SpotifyId}; +use librespot_core::{session::UserData, date::Date, spotify_id::SpotifyItemType, Error, Session, SpotifyId}; pub type AudioItemResult = Result; @@ -93,7 +91,7 @@ pub trait InnerAudioItem { if !(availability .iter() - .any(|availability| Local::now() >= availability.start.as_utc())) + .any(|availability| Date::now_utc() >= availability.start)) { return Err(UnavailabilityReason::Embargo); } diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index d4681c28..d3c4615b 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -1,8 +1,8 @@ -use std::{fmt::Debug, ops::Deref}; +use std::{convert::{TryFrom, TryInto}, fmt::Debug, ops::Deref}; use thiserror::Error; -use crate::util::from_repeated_message; +use crate::util::try_from_repeated_message; use librespot_core::date::Date; @@ -39,13 +39,14 @@ pub enum UnavailabilityReason { NotWhitelisted, } -impl From<&AvailabilityMessage> for Availability { - fn from(availability: &AvailabilityMessage) -> Self { - Self { +impl TryFrom<&AvailabilityMessage> for Availability { + type Error = librespot_core::Error; + fn try_from(availability: &AvailabilityMessage) -> Result { + Ok(Self { catalogue_strs: availability.get_catalogue_str().to_vec(), - start: availability.get_start().into(), - } + start: availability.get_start().try_into()?, + }) } } -from_repeated_message!(AvailabilityMessage, Availabilities); +try_from_repeated_message!(AvailabilityMessage, Availabilities); diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 0eda76ff..d04282ec 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -108,7 +108,7 @@ impl TryFrom<&::Message> for Episode { audio: episode.get_audio().into(), description: episode.get_description().to_owned(), number: episode.get_number(), - publish_time: episode.get_publish_time().into(), + publish_time: episode.get_publish_time().try_into()?, covers: episode.get_cover_image().get_image().into(), language: episode.get_language().to_owned(), is_explicit: episode.get_explicit().to_owned(), @@ -120,7 +120,7 @@ impl TryFrom<&::Message> for Episode { 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(), + availability: episode.get_availability().try_into()?, external_url: episode.get_external_url().to_owned(), episode_type: episode.get_field_type(), has_music_and_talk: episode.get_music_and_talk(), diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs index af6b58ac..053d5e1c 100644 --- a/metadata/src/sale_period.rs +++ b/metadata/src/sale_period.rs @@ -1,6 +1,6 @@ -use std::{fmt::Debug, ops::Deref}; +use std::{convert::{TryFrom, TryInto}, fmt::Debug, ops::Deref}; -use crate::{restriction::Restrictions, util::from_repeated_message}; +use crate::{restriction::Restrictions, util::try_from_repeated_message}; use librespot_core::date::Date; @@ -24,14 +24,15 @@ impl Deref for SalePeriods { } } -impl From<&SalePeriodMessage> for SalePeriod { - fn from(sale_period: &SalePeriodMessage) -> Self { - Self { +impl TryFrom<&SalePeriodMessage> for SalePeriod { + type Error = librespot_core::Error; + fn try_from(sale_period: &SalePeriodMessage) -> Result { + Ok(Self { restrictions: sale_period.get_restriction().into(), - start: sale_period.get_start().into(), - end: sale_period.get_end().into(), - } + start: sale_period.get_start().try_into()?, + end: sale_period.get_end().try_into()?, + }) } } -from_repeated_message!(SalePeriodMessage, SalePeriods); +try_from_repeated_message!(SalePeriodMessage, SalePeriods); diff --git a/metadata/src/show.rs b/metadata/src/show.rs index 9f84ba21..19e910d8 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -65,7 +65,7 @@ impl TryFrom<&::Message> for Show { keywords: show.get_keyword().to_vec(), media_type: show.get_media_type(), consumption_order: show.get_consumption_order(), - availability: show.get_availability().into(), + availability: show.get_availability().try_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/metadata/src/track.rs b/metadata/src/track.rs index df1db8d1..4808b3f1 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -4,7 +4,6 @@ use std::{ ops::Deref, }; -use chrono::Local; use uuid::Uuid; use crate::{ @@ -77,7 +76,7 @@ impl InnerAudioItem for Track { }; // TODO: check meaning of earliest_live_timestamp in - let availability = if Local::now() < track.earliest_live_timestamp.as_utc() { + let availability = if Date::now_utc() < track.earliest_live_timestamp { Err(UnavailabilityReason::Embargo) } else { Self::available_for_user( @@ -130,12 +129,12 @@ impl TryFrom<&::Message> for Track { restrictions: track.get_restriction().into(), files: track.get_file().into(), alternatives: track.get_alternative().try_into()?, - sale_periods: track.get_sale_period().into(), + sale_periods: track.get_sale_period().try_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(), + availability: track.get_availability().try_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(), From 8811b89b2d243c6b5ffba6246bf65eec1e3972b3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Jan 2022 23:45:31 +0100 Subject: [PATCH 112/147] Document MSRV 1.53 and `cargo clippy` requirement --- COMPILING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/COMPILING.md b/COMPILING.md index 39ae20cc..6f390447 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -7,7 +7,7 @@ In order to compile librespot, you will first need to set up a suitable Rust bui ### Install Rust 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.* +*Note: The current minimum required Rust version at the time of writing is 1.53.* #### Additional Rust tools - `rustfmt` 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: @@ -15,7 +15,7 @@ To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust- rustup component add rustfmt rustup component add clippy ``` -Using `rustfmt` is not optional, as our CI checks against this repo's rules. +Using `cargo fmt` and `cargo clippy` is not optional, as our CI checks against this repo's rules. ### General dependencies Along with Rust, you will also require a C compiler. @@ -63,7 +63,7 @@ sudo dnf install alsa-lib-devel The recommended method is to first fork the repo, so that you have a copy that you have read/write access to. After that, it’s a simple case of cloning your fork. ```bash -git clone git@github.com:YOURUSERNAME/librespot.git +git clone git@github.com:YOUR_USERNAME/librespot.git ``` ## Compiling & Running From abbc3bade8c8b03870563a1a2a562718c3082c9e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 16 Jan 2022 01:14:00 +0100 Subject: [PATCH 113/147] Register message listeners before connecting --- connect/src/spirc.rs | 53 ++++++++++------- core/src/mercury/mod.rs | 44 +++++++------- core/src/session.rs | 56 ++++++++++-------- src/main.rs | 126 +++++++++++++++++----------------------- 4 files changed, 141 insertions(+), 138 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index af8189de..cb87582e 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -8,7 +8,7 @@ use std::{ use futures_util::{ future::{self, FusedFuture}, stream::FusedStream, - FutureExt, StreamExt, TryFutureExt, + FutureExt, StreamExt, }; use protobuf::{self, Message}; @@ -21,6 +21,7 @@ use crate::{ config::ConnectConfig, context::StationContext, core::{ + authentication::Credentials, mercury::{MercuryError, MercurySender}, session::UserAttributes, util::SeqGenerator, @@ -92,7 +93,7 @@ struct SpircTask { play_request_id: Option, play_status: SpircPlayStatus, - remote_update: BoxedStream>, + remote_update: BoxedStream>, connection_id_update: BoxedStream>, user_attributes_update: BoxedStream>, user_attributes_mutation: BoxedStream>, @@ -255,9 +256,10 @@ fn url_encode(bytes: impl AsRef<[u8]>) -> String { } impl Spirc { - pub fn new( + pub async fn new( config: ConnectConfig, session: Session, + credentials: Credentials, player: Player, mixer: Box, ) -> Result<(Spirc, impl Future), Error> { @@ -265,23 +267,21 @@ impl Spirc { let ident = session.device_id().to_owned(); - // Uri updated in response to issue #288 - let canonical_username = &session.username(); - debug!("canonical_username: {}", canonical_username); - let uri = format!("hm://remote/user/{}/", url_encode(canonical_username)); - let remote_update = Box::pin( session .mercury() - .subscribe(uri.clone()) - .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 + .listen_for("hm://remote/user/") .map(UnboundedReceiverStream::new) .flatten_stream() - .map(|response| -> Result { + .map(|response| -> Result<(String, Frame), Error> { + let uri_split: Vec<&str> = response.uri.split('/').collect(); + let username = match uri_split.get(uri_split.len() - 2) { + Some(s) => s.to_string(), + None => String::new(), + }; + let data = response.payload.first().ok_or(SpircError::NoData)?; - Ok(Frame::parse_from_bytes(data)?) + Ok((username, Frame::parse_from_bytes(data)?)) }), ); @@ -324,7 +324,14 @@ impl Spirc { }), ); - let sender = session.mercury().sender(uri); + // Connect *after* all message listeners are registered + session.connect(credentials).await?; + + let canonical_username = &session.username(); + debug!("canonical_username: {}", canonical_username); + let sender_uri = format!("hm://remote/user/{}/", url_encode(canonical_username)); + + let sender = session.mercury().sender(sender_uri); let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); @@ -414,13 +421,17 @@ impl SpircTask { tokio::select! { 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); - } + Ok((username, frame)) => { + if username != self.session.username() { + error!("could not dispatch remote update: frame was intended for {}", username); + } else if let Err(e) = self.handle_remote_update(frame) { + error!("could not dispatch remote update: {}", e); + } + }, Err(e) => error!("could not parse remote update: {}", e), } None => { - error!("subscription terminated"); + error!("remote update selected, but none received"); break; } }, @@ -513,7 +524,7 @@ impl SpircTask { } if self.sender.flush().await.is_err() { - warn!("Cannot flush spirc event sender."); + warn!("Cannot flush spirc event sender when done."); } } @@ -754,7 +765,7 @@ impl SpircTask { } fn handle_remote_update(&mut self, update: Frame) -> Result<(), Error> { - trace!("Received update frame: {:#?}", update,); + trace!("Received update frame: {:#?}", update); // First see if this update was intended for us. let device_id = &self.ident; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index b693444a..44e8de9c 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -248,21 +248,21 @@ impl MercuryManager { } Err(MercuryError::Response(response).into()) } else if let PacketType::MercuryEvent = cmd { + // TODO: This is just a workaround to make utf-8 encoded usernames work. + // A better solution would be to use an uri struct and urlencode it directly + // 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_or_default().to_string()) + .chain(uri_split.map(|component| { + form_urlencoded::byte_serialize(component.as_bytes()).collect::() + })) + .collect::>() + .join("/"); + + let mut found = false; + self.lock(|inner| { - let mut found = false; - - // TODO: This is just a workaround to make utf-8 encoded usernames work. - // A better solution would be to use an uri struct and urlencode it directly - // 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_or_default().to_string()) - .chain(uri_split.map(|component| { - form_urlencoded::byte_serialize(component.as_bytes()).collect::() - })) - .collect::>() - .join("/"); - inner.subscriptions.retain(|&(ref prefix, ref sub)| { if encoded_uri.starts_with(prefix) { found = true; @@ -275,15 +275,15 @@ impl MercuryManager { true } }); + }); - if !found { - debug!("unknown subscription uri={}", &response.uri); - trace!("response pushed over Mercury: {:?}", response); - Err(MercuryError::Response(response).into()) - } else { - Ok(()) - } - }) + if !found { + 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 { cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?; Ok(()) diff --git a/core/src/session.rs b/core/src/session.rs index 913f5813..e4d11f7f 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -46,6 +46,8 @@ pub enum SessionError { AuthenticationError(#[from] AuthenticationError), #[error("Cannot create session: {0}")] IoError(#[from] io::Error), + #[error("Session is not connected")] + NotConnected, #[error("packet {0} unknown")] Packet(u8), } @@ -55,6 +57,7 @@ impl From for Error { match err { SessionError::AuthenticationError(_) => Error::unauthenticated(err), SessionError::IoError(_) => Error::unavailable(err), + SessionError::NotConnected => Error::unavailable(err), SessionError::Packet(_) => Error::unimplemented(err), } } @@ -83,7 +86,7 @@ struct SessionInternal { data: RwLock, http_client: HttpClient, - tx_connection: mpsc::UnboundedSender<(u8, Vec)>, + tx_connection: OnceCell)>>, apresolver: OnceCell, audio_key: OnceCell, @@ -104,22 +107,17 @@ static SESSION_COUNTER: AtomicUsize = AtomicUsize::new(0); pub struct Session(Arc); impl Session { - pub async fn connect( - config: SessionConfig, - credentials: Credentials, - cache: Option, - ) -> Result { + pub fn new(config: SessionConfig, cache: Option) -> Self { 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::AcqRel); + let session_id = SESSION_COUNTER.fetch_add(1, Ordering::AcqRel); debug!("new Session[{}]", session_id); - let session = Session(Arc::new(SessionInternal { + Self(Arc::new(SessionInternal { config, data: RwLock::new(SessionData::default()), http_client, - tx_connection: sender_tx, + tx_connection: OnceCell::new(), cache: cache.map(Arc::new), apresolver: OnceCell::new(), audio_key: OnceCell::new(), @@ -129,27 +127,33 @@ impl Session { token_provider: OnceCell::new(), handle: tokio::runtime::Handle::current(), session_id, - })); + })) + } - let ap = session.apresolver().resolve("accesspoint").await?; + pub async fn connect(&self, credentials: Credentials) -> Result<(), Error> { + let ap = self.apresolver().resolve("accesspoint").await?; info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); - let mut transport = - connection::connect(&ap.0, ap.1, session.config().proxy.as_ref()).await?; + let mut transport = connection::connect(&ap.0, ap.1, self.config().proxy.as_ref()).await?; let reusable_credentials = - connection::authenticate(&mut transport, credentials, &session.config().device_id) - .await?; + connection::authenticate(&mut transport, credentials, &self.config().device_id).await?; info!("Authenticated as \"{}\" !", reusable_credentials.username); - session.0.data.write().user_data.canonical_username = reusable_credentials.username.clone(); - if let Some(cache) = session.cache() { + self.set_username(&reusable_credentials.username); + if let Some(cache) = self.cache() { cache.save_credentials(&reusable_credentials); } + let (tx_connection, rx_connection) = mpsc::unbounded_channel(); + self.0 + .tx_connection + .set(tx_connection) + .map_err(|_| SessionError::NotConnected)?; + let (sink, stream) = transport.split(); - let sender_task = UnboundedReceiverStream::new(sender_rx) + let sender_task = UnboundedReceiverStream::new(rx_connection) .map(Ok) .forward(sink); - let receiver_task = DispatchTask(stream, session.weak()); + let receiver_task = DispatchTask(stream, self.weak()); tokio::spawn(async move { let result = future::try_join(sender_task, receiver_task).await; @@ -159,7 +163,7 @@ impl Session { } }); - Ok(session) + Ok(()) } pub fn apresolver(&self) -> &ApResolver { @@ -323,8 +327,10 @@ impl Session { } pub fn send_packet(&self, cmd: PacketType, data: Vec) -> Result<(), Error> { - self.0.tx_connection.send((cmd as u8, data))?; - Ok(()) + match self.0.tx_connection.get() { + Some(tx) => Ok(tx.send((cmd as u8, data))?), + None => Err(SessionError::NotConnected.into()), + } } pub fn cache(&self) -> Option<&Arc> { @@ -366,6 +372,10 @@ impl Session { self.0.data.read().user_data.canonical_username.clone() } + pub fn set_username(&self, username: &str) { + self.0.data.write().user_data.canonical_username = username.to_owned(); + } + pub fn country(&self) -> String { self.0.data.read().user_data.country.clone() } diff --git a/src/main.rs b/src/main.rs index 527a234b..2919ade7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use std::{ time::{Duration, Instant}, }; -use futures_util::{future, FutureExt, StreamExt}; +use futures_util::StreamExt; use log::{error, info, trace, warn}; use sha1::{Digest, Sha1}; use thiserror::Error; @@ -1562,7 +1562,9 @@ async fn main() { let mut player_event_channel: Option> = None; let mut auto_connect_times: Vec = vec![]; let mut discovery = None; - let mut connecting: Pin>> = Box::pin(future::pending()); + let mut connecting = false; + + let session = Session::new(setup.session_config.clone(), setup.cache.clone()); if setup.enable_discovery { let device_id = setup.session_config.device_id.clone(); @@ -1582,15 +1584,8 @@ async fn main() { } if let Some(credentials) = setup.credentials { - last_credentials = Some(credentials.clone()); - connecting = Box::pin( - Session::connect( - setup.session_config.clone(), - credentials, - setup.cache.clone(), - ) - .fuse(), - ); + last_credentials = Some(credentials); + connecting = true; } loop { @@ -1616,11 +1611,7 @@ async fn main() { tokio::spawn(spirc_task); } - connecting = Box::pin(Session::connect( - setup.session_config.clone(), - credentials, - setup.cache.clone(), - ).fuse()); + connecting = true; }, None => { error!("Discovery stopped unexpectedly"); @@ -1628,63 +1619,59 @@ async fn main() { } } }, - session = &mut connecting, if !connecting.is_terminated() => match session { - Ok(session) => { - let mixer_config = setup.mixer_config.clone(); - let mixer = (setup.mixer)(mixer_config); - let player_config = setup.player_config.clone(); - let connect_config = setup.connect_config.clone(); + _ = async {}, if connecting && last_credentials.is_some() => { + let mixer_config = setup.mixer_config.clone(); + let mixer = (setup.mixer)(mixer_config); + let player_config = setup.player_config.clone(); + let connect_config = setup.connect_config.clone(); - let audio_filter = mixer.get_audio_filter(); - let format = setup.format; - let backend = setup.backend; - let device = setup.device.clone(); - let (player, event_channel) = - Player::new(player_config, session.clone(), audio_filter, move || { - (backend)(device, format) - }); + let audio_filter = mixer.get_audio_filter(); + let format = setup.format; + let backend = setup.backend; + let device = setup.device.clone(); + let (player, event_channel) = + Player::new(player_config, session.clone(), audio_filter, move || { + (backend)(device, format) + }); - if setup.emit_sink_events { - if let Some(player_event_program) = setup.player_event_program.clone() { - player.set_sink_event_callback(Some(Box::new(move |sink_status| { - match emit_sink_event(sink_status, &player_event_program) { - Ok(e) if e.success() => (), - Ok(e) => { - if let Some(code) = e.code() { - warn!("Sink event program returned exit code {}", code); - } else { - warn!("Sink event program returned failure"); - } - }, - Err(e) => { - warn!("Emitting sink event failed: {}", e); - }, - } - }))); - } - }; + if setup.emit_sink_events { + if let Some(player_event_program) = setup.player_event_program.clone() { + player.set_sink_event_callback(Some(Box::new(move |sink_status| { + match emit_sink_event(sink_status, &player_event_program) { + Ok(e) if e.success() => (), + Ok(e) => { + if let Some(code) = e.code() { + warn!("Sink event program returned exit code {}", code); + } else { + warn!("Sink event program returned failure"); + } + }, + Err(e) => { + warn!("Emitting sink event failed: {}", e); + }, + } + }))); + } + }; - 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); - }, - Err(e) => { - error!("Connection failed: {}", e); - exit(1); - } + let (spirc_, spirc_task_) = match Spirc::new(connect_config, session.clone(), last_credentials.clone().unwrap(), player, mixer).await { + 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); + + connecting = false; }, _ = async { if let Some(task) = spirc_task.as_mut() { task.await; } - }, if spirc_task.is_some() => { + }, if spirc_task.is_some() && !connecting => { spirc_task = None; warn!("Spirc shut down unexpectedly"); @@ -1695,14 +1682,9 @@ async fn main() { }; match last_credentials.clone() { - Some(credentials) if !reconnect_exceeds_rate_limit() => { + Some(_) if !reconnect_exceeds_rate_limit() => { auto_connect_times.push(Instant::now()); - - connecting = Box::pin(Session::connect( - setup.session_config.clone(), - credentials, - setup.cache.clone(), - ).fuse()); + connecting = true; }, _ => { error!("Spirc shut down too often. Not reconnecting automatically."); From 2065ded7b6347334d8abeecd52e53d677dfc754a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 16 Jan 2022 01:29:50 +0100 Subject: [PATCH 114/147] Fix examples --- examples/get_token.rs | 21 ++++++++++----------- examples/play.rs | 29 +++++++++++++++++------------ examples/playlist_tracks.rs | 21 ++++++++++++--------- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/examples/get_token.rs b/examples/get_token.rs index 3ef6bd71..a568ae07 100644 --- a/examples/get_token.rs +++ b/examples/get_token.rs @@ -1,8 +1,6 @@ use std::env; -use librespot::core::authentication::Credentials; -use librespot::core::config::SessionConfig; -use librespot::core::session::Session; +use librespot::core::{authentication::Credentials, config::SessionConfig, session::Session}; const SCOPES: &str = "streaming,user-read-playback-state,user-modify-playback-state,user-read-currently-playing"; @@ -17,14 +15,15 @@ async fn main() { return; } - println!("Connecting.."); + println!("Connecting..."); let credentials = Credentials::with_password(&args[1], &args[2]); - let session = Session::connect(session_config, credentials, None) - .await - .unwrap(); + let session = Session::new(session_config, None); - println!( - "Token: {:#?}", - session.token_provider().get_token(SCOPES).await.unwrap() - ); + match session.connect(credentials).await { + Ok(()) => println!( + "Token: {:#?}", + session.token_provider().get_token(SCOPES).await.unwrap() + ), + Err(e) => println!("Error connecting: {}", e), + } } diff --git a/examples/play.rs b/examples/play.rs index d6c7196d..3cbbc43b 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -1,12 +1,15 @@ -use std::env; +use std::{env, process::exit}; -use librespot::core::authentication::Credentials; -use librespot::core::config::SessionConfig; -use librespot::core::session::Session; -use librespot::core::spotify_id::SpotifyId; -use librespot::playback::audio_backend; -use librespot::playback::config::{AudioFormat, PlayerConfig}; -use librespot::playback::player::Player; +use librespot::{ + core::{ + authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId, + }, + playback::{ + audio_backend, + config::{AudioFormat, PlayerConfig}, + player::Player, + }, +}; #[tokio::main] async fn main() { @@ -25,10 +28,12 @@ async fn main() { let backend = audio_backend::find(None).unwrap(); - println!("Connecting .."); - let session = Session::connect(session_config, credentials, None) - .await - .unwrap(); + println!("Connecting..."); + let session = Session::new(session_config, None); + if let Err(e) = session.connect(credentials).await { + println!("Error connecting: {}", e); + exit(1); + } let (mut player, _) = Player::new(player_config, session, None, move || { backend(None, audio_format) diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 0b19e73e..2f53a8a3 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -1,10 +1,11 @@ -use std::env; +use std::{env, process::exit}; -use librespot::core::authentication::Credentials; -use librespot::core::config::SessionConfig; -use librespot::core::session::Session; -use librespot::core::spotify_id::SpotifyId; -use librespot::metadata::{Metadata, Playlist, Track}; +use librespot::{ + core::{ + authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId, + }, + metadata::{Metadata, Playlist, Track}, +}; #[tokio::main] async fn main() { @@ -24,9 +25,11 @@ async fn main() { let plist_uri = SpotifyId::from_base62(uri_parts[2]).unwrap(); - let session = Session::connect(session_config, credentials, None) - .await - .unwrap(); + let session = Session::new(session_config, None); + if let Err(e) = session.connect(credentials).await { + println!("Error connecting: {}", e); + exit(1); + } let plist = Playlist::get(&session, plist_uri).await.unwrap(); println!("{:?}", plist); From fcb21df81f6c1cef4e36069d8a29a746cef29c64 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 16 Jan 2022 01:36:28 +0100 Subject: [PATCH 115/147] Fix connect test --- core/tests/connect.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/core/tests/connect.rs b/core/tests/connect.rs index 19d7977e..c76ba7ce 100644 --- a/core/tests/connect.rs +++ b/core/tests/connect.rs @@ -1,20 +1,15 @@ use std::time::Duration; -use librespot_core::authentication::Credentials; -use librespot_core::config::SessionConfig; -use librespot_core::session::Session; - use tokio::time::timeout; +use librespot_core::{authentication::Credentials, config::SessionConfig, session::Session}; + #[tokio::test] async fn test_connection() { timeout(Duration::from_secs(30), async { - let result = Session::connect( - SessionConfig::default(), - Credentials::with_password("test", "test"), - None, - ) - .await; + let result = Session::new(SessionConfig::default(), None) + .connect(Credentials::with_password("test", "test")) + .await; match result { Ok(_) => panic!("Authentication succeeded despite of bad credentials."), From 8851951f04a748347f58b950d5c47bf1736b9148 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 16 Jan 2022 21:29:59 +0100 Subject: [PATCH 116/147] Change counting to `spirc` and `player` They can be reinstantiated, unlike the `session` which is now intended to be constructed once. --- connect/src/spirc.rs | 12 ++++++++++-- core/src/session.rs | 24 +++++------------------- playback/src/player.rs | 21 +++++++++++++++------ 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index cb87582e..1cc07cde 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -2,6 +2,7 @@ use std::{ convert::TryFrom, future::Future, pin::Pin, + sync::atomic::{AtomicUsize, Ordering}, time::{SystemTime, UNIX_EPOCH}, }; @@ -106,8 +107,12 @@ struct SpircTask { context_fut: BoxedFuture>, autoplay_fut: BoxedFuture>, context: Option, + + spirc_id: usize, } +static SPIRC_COUNTER: AtomicUsize = AtomicUsize::new(0); + pub enum SpircCommand { Play, PlayPause, @@ -263,7 +268,8 @@ impl Spirc { player: Player, mixer: Box, ) -> Result<(Spirc, impl Future), Error> { - debug!("new Spirc[{}]", session.session_id()); + let spirc_id = SPIRC_COUNTER.fetch_add(1, Ordering::AcqRel); + debug!("new Spirc[{}]", spirc_id); let ident = session.device_id().to_owned(); @@ -368,6 +374,8 @@ impl Spirc { context_fut: Box::pin(future::pending()), autoplay_fut: Box::pin(future::pending()), context: None, + + spirc_id, }; if let Some(volume) = initial_volume { @@ -1427,7 +1435,7 @@ impl SpircTask { impl Drop for SpircTask { fn drop(&mut self) { - debug!("drop Spirc[{}]", self.session.session_id()); + debug!("drop Spirc[{}]", self.spirc_id); } } diff --git a/core/src/session.rs b/core/src/session.rs index e4d11f7f..b222d32e 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -4,10 +4,7 @@ use std::{ io, pin::Pin, process::exit, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, Weak, - }, + sync::{Arc, Weak}, task::{Context, Poll}, time::{SystemTime, UNIX_EPOCH}, }; @@ -97,12 +94,8 @@ struct SessionInternal { cache: Option>, handle: tokio::runtime::Handle, - - session_id: usize, } -static SESSION_COUNTER: AtomicUsize = AtomicUsize::new(0); - #[derive(Clone)] pub struct Session(Arc); @@ -110,8 +103,7 @@ impl Session { pub fn new(config: SessionConfig, cache: Option) -> Self { let http_client = HttpClient::new(config.proxy.as_ref()); - let session_id = SESSION_COUNTER.fetch_add(1, Ordering::AcqRel); - debug!("new Session[{}]", session_id); + debug!("new Session"); Self(Arc::new(SessionInternal { config, @@ -126,7 +118,6 @@ impl Session { spclient: OnceCell::new(), token_provider: OnceCell::new(), handle: tokio::runtime::Handle::current(), - session_id, })) } @@ -218,8 +209,7 @@ impl Session { fn debug_info(&self) { debug!( - "Session[{}] strong={} weak={}", - self.0.session_id, + "Session strong={} weak={}", Arc::strong_count(&self.0), Arc::weak_count(&self.0) ); @@ -413,12 +403,8 @@ impl Session { SessionWeak(Arc::downgrade(&self.0)) } - pub fn session_id(&self) -> usize { - self.0.session_id - } - pub fn shutdown(&self) { - debug!("Invalidating session [{}]", self.0.session_id); + debug!("Invalidating session"); self.0.data.write().invalid = true; self.mercury().shutdown(); self.channel().shutdown(); @@ -445,7 +431,7 @@ impl SessionWeak { impl Drop for SessionInternal { fn drop(&mut self) { - debug!("drop Session[{}]", self.session_id); + debug!("drop Session"); } } diff --git a/playback/src/player.rs b/playback/src/player.rs index aad6df5b..a52cc01b 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -7,7 +7,10 @@ use std::{ mem, pin::Pin, process::exit, - sync::Arc, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, task::{Context, Poll}, thread, time::{Duration, Instant}, @@ -84,8 +87,12 @@ struct PlayerInternal { normalisation_peak: f64, auto_normalise_as_album: bool, + + player_id: usize, } +static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0); + enum PlayerCommand { Load { track_id: SpotifyId, @@ -365,7 +372,8 @@ impl Player { } let handle = thread::spawn(move || { - debug!("new Player[{}]", session.session_id()); + let player_id = PLAYER_COUNTER.fetch_add(1, Ordering::AcqRel); + debug!("new Player [{}]", player_id); let converter = Converter::new(config.ditherer); @@ -388,6 +396,8 @@ impl Player { normalisation_integrator: 0.0, auto_normalise_as_album: false, + + player_id, }; // While PlayerInternal is written as a future, it still contains blocking code. @@ -488,9 +498,8 @@ impl Drop for Player { debug!("Shutting down player thread ..."); self.commands = None; if let Some(handle) = self.thread_handle.take() { - match handle.join() { - Ok(_) => (), - Err(e) => error!("Player thread Error: {:?}", e), + if let Err(e) = handle.join() { + error!("Player thread Error: {:?}", e); } } } @@ -2043,7 +2052,7 @@ impl PlayerInternal { impl Drop for PlayerInternal { fn drop(&mut self) { - debug!("drop PlayerInternal[{}]", self.session.session_id()); + debug!("drop PlayerInternal[{}]", self.player_id); let handles: Vec> = { // waiting for the thread while holding the mutex would result in a deadlock From f2625965b3e5d5dd5a66822d3b2829aa08ffcfa3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 22 Jan 2022 21:13:11 +0100 Subject: [PATCH 117/147] Count from the start for stability --- connect/src/spirc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 1cc07cde..bea95c57 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -281,7 +281,7 @@ impl Spirc { .flatten_stream() .map(|response| -> Result<(String, Frame), Error> { let uri_split: Vec<&str> = response.uri.split('/').collect(); - let username = match uri_split.get(uri_split.len() - 2) { + let username = match uri_split.get(4) { Some(s) => s.to_string(), None => String::new(), }; From 0822af032822944e6a57abe9225d95cbce679ab5 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 22 Jan 2022 21:17:55 +0100 Subject: [PATCH 118/147] Use configured client ID on initial connection (fixes #941) --- core/src/session.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/session.rs b/core/src/session.rs index b222d32e..76571a94 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -105,9 +105,14 @@ impl Session { debug!("new Session"); + let session_data = SessionData { + client_id: config.client_id.clone(), + ..SessionData::default() + }; + Self(Arc::new(SessionInternal { config, - data: RwLock::new(SessionData::default()), + data: RwLock::new(session_data), http_client, tx_connection: OnceCell::new(), cache: cache.map(Arc::new), From 0630586cd659d428a5b8dbd6c4228db842fbc8eb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 22 Jan 2022 21:27:56 +0100 Subject: [PATCH 119/147] Ensure a client ID is present --- core/src/token.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/token.rs b/core/src/token.rs index f7c8d350..2c88b2e0 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -65,6 +65,11 @@ impl TokenProvider { // scopes must be comma-separated pub async fn get_token(&self, scopes: &str) -> Result { + let client_id = self.session().client_id(); + if client_id.is_empty() { + return Err(Error::invalid_argument("Client ID cannot be empty")); + } + 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() { @@ -82,7 +87,7 @@ impl TokenProvider { let query_uri = format!( "hm://keymaster/token/authenticated?scope={}&client_id={}&device_id={}", scopes, - self.session().client_id(), + client_id, self.session().device_id(), ); let request = self.session().mercury().get(query_uri)?; From 15282925836dba199baa69b81571b14f29ba872e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 22 Jan 2022 23:17:10 +0100 Subject: [PATCH 120/147] Retrieve client token (not working) --- core/src/spclient.rs | 59 +++++++++++++++++-- protocol/build.rs | 2 + protocol/proto/connectivity.proto | 13 +++- .../clienttoken/v0/clienttoken_http.proto | 9 ++- 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 1aa0da00..5ca736d9 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -5,7 +5,7 @@ use futures_util::future::IntoStream; use http::header::HeaderValue; use hyper::{ client::ResponseFuture, - header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, + header::{ACCEPT, AUTHORIZATION, CONTENT_ENCODING, CONTENT_TYPE, RANGE}, Body, HeaderMap, Method, Request, }; use protobuf::Message; @@ -17,16 +17,19 @@ use crate::{ cdn_url::CdnUrl, error::ErrorKind, protocol::{ - canvaz::EntityCanvazRequest, connect::PutStateRequest, + canvaz::EntityCanvazRequest, + clienttoken_http::{ClientTokenRequest, ClientTokenRequestType, ClientTokenResponse}, + connect::PutStateRequest, extended_metadata::BatchedEntityRequest, }, - Error, FileId, SpotifyId, + version, Error, FileId, SpotifyId, }; component! { SpClient : SpClientInner { accesspoint: Option = None, strategy: RequestStrategy = RequestStrategy::default(), + client_token: String = String::new(), } } @@ -88,6 +91,51 @@ impl SpClient { Ok(format!("https://{}:{}", ap.0, ap.1)) } + pub async fn client_token(&self) -> Result { + // TODO: implement expiry + let client_token = self.lock(|inner| inner.client_token.clone()); + if !client_token.is_empty() { + return Ok(client_token); + } + + let mut message = ClientTokenRequest::new(); + message.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); + + let client_data = message.mut_client_data(); + client_data.set_client_id(self.session().client_id()); + client_data.set_client_version(version::SEMVER.to_string()); + + let connectivity_data = client_data.mut_connectivity_sdk_data(); + connectivity_data.set_device_id(self.session().device_id().to_string()); + + let platform_data = connectivity_data.mut_platform_specific_data(); + let windows_data = platform_data.mut_windows(); + windows_data.set_os_version(10); + windows_data.set_os_build(21370); + windows_data.set_unknown_value_4(2); + windows_data.set_unknown_value_6(9); + windows_data.set_unknown_value_7(332); + windows_data.set_unknown_value_8(34404); + windows_data.set_unknown_value_10(true); + + let body = protobuf::text_format::print_to_string(&message); + + let request = Request::builder() + .method(&Method::POST) + .uri("https://clienttoken.spotify.com/v1/clienttoken") + .header(ACCEPT, HeaderValue::from_static("application/x-protobuf")) + .header(CONTENT_ENCODING, HeaderValue::from_static("")) + .body(Body::from(body))?; + + let response = self.session().http_client().request_body(request).await?; + let response = ClientTokenResponse::parse_from_bytes(&response)?; + + let client_token = response.get_granted_token().get_token().to_owned(); + self.lock(|inner| inner.client_token = client_token.clone()); + + Ok(client_token) + } + pub async fn request_with_protobuf( &self, method: &Method, @@ -100,7 +148,7 @@ impl SpClient { let mut headers = headers.unwrap_or_else(HeaderMap::new); headers.insert( CONTENT_TYPE, - HeaderValue::from_static("application/protobuf"), + HeaderValue::from_static("application/x-protobuf"), ); self.request(method, endpoint, Some(headers), Some(body)) @@ -132,6 +180,9 @@ impl SpClient { let body = body.unwrap_or_else(String::new); + let client_token = self.client_token().await; + trace!("CLIENT TOKEN: {:?}", client_token); + loop { tries += 1; diff --git a/protocol/build.rs b/protocol/build.rs index aa107607..b7c3f44d 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -17,6 +17,7 @@ fn compile() { let files = &[ proto_dir.join("connect.proto"), + proto_dir.join("connectivity.proto"), proto_dir.join("devices.proto"), proto_dir.join("entity_extension_data.proto"), proto_dir.join("extended_metadata.proto"), @@ -26,6 +27,7 @@ fn compile() { proto_dir.join("playlist_annotate3.proto"), proto_dir.join("playlist_permission.proto"), proto_dir.join("playlist4_external.proto"), + proto_dir.join("spotify/clienttoken/v0/clienttoken_http.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 diff --git a/protocol/proto/connectivity.proto b/protocol/proto/connectivity.proto index f7e64a3c..757f48c4 100644 --- a/protocol/proto/connectivity.proto +++ b/protocol/proto/connectivity.proto @@ -1,5 +1,3 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - syntax = "proto3"; package spotify.clienttoken.data.v0; @@ -17,6 +15,7 @@ message PlatformSpecificData { oneof data { NativeAndroidData android = 1; NativeIOSData ios = 2; + NativeWindowsData windows = 4; } } @@ -36,6 +35,16 @@ message NativeIOSData { string simulator_model_identifier = 5; } +message NativeWindowsData { + int32 os_version = 1; + int32 os_build = 3; + int32 unknown_value_4 = 4; + int32 unknown_value_6 = 6; + int32 unknown_value_7 = 7; + int32 unknown_value_8 = 8; + bool unknown_value_10 = 10; +} + message Screen { int32 width = 1; int32 height = 2; diff --git a/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto b/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto index 92d50f42..c60cdcaf 100644 --- a/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto +++ b/protocol/proto/spotify/clienttoken/v0/clienttoken_http.proto @@ -1,5 +1,3 @@ -// Extracted from: Spotify 1.1.33.569 (Windows) - syntax = "proto3"; package spotify.clienttoken.http.v0; @@ -24,7 +22,7 @@ message ClientDataRequest { string client_id = 2; oneof data { - data.v0.ConnectivitySdkData connectivity_sdk_data = 3; + spotify.clienttoken.data.v0.ConnectivitySdkData connectivity_sdk_data = 3; } } @@ -42,10 +40,15 @@ message ClientTokenResponse { } } +message TokenDomain { + string domain = 1; +} + message GrantedTokenResponse { string token = 1; int32 expires_after_seconds = 2; int32 refresh_after_seconds = 3; + repeated TokenDomain domains = 4; } message ChallengesResponse { From 4ea1b77c7bf6f64e4472340670a7c84758943bb6 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 23 Jan 2022 00:26:52 +0100 Subject: [PATCH 121/147] Fix `client-token` and implement expiry logic --- core/src/spclient.rs | 76 ++++++++++++++++++++++++++++++++------------ core/src/token.rs | 4 +-- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 5ca736d9..6217883e 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -1,4 +1,7 @@ -use std::time::Duration; +use std::{ + convert::TryInto, + time::{Duration, Instant}, +}; use bytes::Bytes; use futures_util::future::IntoStream; @@ -22,6 +25,7 @@ use crate::{ connect::PutStateRequest, extended_metadata::BatchedEntityRequest, }, + token::Token, version, Error, FileId, SpotifyId, }; @@ -29,7 +33,7 @@ component! { SpClient : SpClientInner { accesspoint: Option = None, strategy: RequestStrategy = RequestStrategy::default(), - client_token: String = String::new(), + client_token: Option = None, } } @@ -92,12 +96,21 @@ impl SpClient { } pub async fn client_token(&self) -> Result { - // TODO: implement expiry - let client_token = self.lock(|inner| inner.client_token.clone()); - if !client_token.is_empty() { - return Ok(client_token); + let client_token = self.lock(|inner| { + if let Some(token) = &inner.client_token { + if token.is_expired() { + inner.client_token = None; + } + } + inner.client_token.clone() + }); + + if let Some(client_token) = client_token { + return Ok(client_token.access_token); } + trace!("Client token unavailable or expired, requesting new token."); + let mut message = ClientTokenRequest::new(); message.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); @@ -118,7 +131,7 @@ impl SpClient { windows_data.set_unknown_value_8(34404); windows_data.set_unknown_value_10(true); - let body = protobuf::text_format::print_to_string(&message); + let body = message.write_to_bytes()?; let request = Request::builder() .method(&Method::POST) @@ -128,10 +141,35 @@ impl SpClient { .body(Body::from(body))?; let response = self.session().http_client().request_body(request).await?; - let response = ClientTokenResponse::parse_from_bytes(&response)?; + let message = ClientTokenResponse::parse_from_bytes(&response)?; - let client_token = response.get_granted_token().get_token().to_owned(); - self.lock(|inner| inner.client_token = client_token.clone()); + let client_token = self.lock(|inner| { + let access_token = message.get_granted_token().get_token().to_owned(); + + let client_token = Token { + access_token: access_token.clone(), + expires_in: Duration::from_secs( + message + .get_granted_token() + .get_refresh_after_seconds() + .try_into() + .unwrap_or(7200), + ), + token_type: "client-token".to_string(), + scopes: message + .get_granted_token() + .get_domains() + .iter() + .map(|d| d.domain.clone()) + .collect(), + timestamp: Instant::now(), + }; + + trace!("Got client token: {:?}", client_token); + + inner.client_token = Some(client_token); + access_token + }); Ok(client_token) } @@ -180,9 +218,6 @@ impl SpClient { let body = body.unwrap_or_else(String::new); - let client_token = self.client_token().await; - trace!("CLIENT TOKEN: {:?}", client_token); - loop { tries += 1; @@ -205,20 +240,19 @@ impl SpClient { .body(Body::from(body.clone()))?; // Reconnection logic: keep getting (cached) tokens because they might have expired. + let token = self + .session() + .token_provider() + .get_token("playlist-read") + .await?; + let headers_mut = request.headers_mut(); if let Some(ref hdrs) = headers { *headers_mut = hdrs.clone(); } headers_mut.insert( AUTHORIZATION, - HeaderValue::from_str(&format!( - "Bearer {}", - self.session() - .token_provider() - .get_token("playlist-read") - .await? - .access_token - ))?, + HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?, ); last_response = self.session().http_client().request_body(request).await; diff --git a/core/src/token.rs b/core/src/token.rs index 2c88b2e0..02f94b60 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -93,7 +93,7 @@ impl TokenProvider { let request = self.session().mercury().get(query_uri)?; let response = request.await?; let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec(); - let token = Token::new(String::from_utf8(data)?)?; + let token = Token::from_json(String::from_utf8(data)?)?; trace!("Got token: {:#?}", token); self.lock(|inner| inner.tokens.push(token.clone())); Ok(token) @@ -103,7 +103,7 @@ impl TokenProvider { impl Token { const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10); - pub fn new(body: String) -> Result { + pub fn from_json(body: String) -> Result { let data: TokenData = serde_json::from_slice(body.as_ref())?; Ok(Self { access_token: data.access_token, From 8498ad807829b0b273c8bbd1c01b04fa10146d72 Mon Sep 17 00:00:00 2001 From: SuisChan Date: Mon, 24 Jan 2022 12:52:15 +0100 Subject: [PATCH 122/147] Update connectivity.proto --- core/src/spclient.rs | 6 +++--- protocol/proto/connectivity.proto | 25 +++++++++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 6217883e..7bfa1064 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -125,10 +125,10 @@ impl SpClient { let windows_data = platform_data.mut_windows(); windows_data.set_os_version(10); windows_data.set_os_build(21370); - windows_data.set_unknown_value_4(2); + windows_data.set_platform_id(2); windows_data.set_unknown_value_6(9); - windows_data.set_unknown_value_7(332); - windows_data.set_unknown_value_8(34404); + windows_data.set_image_file_machine(332); + windows_data.set_pe_machine(34404); windows_data.set_unknown_value_10(true); let body = message.write_to_bytes()?; diff --git a/protocol/proto/connectivity.proto b/protocol/proto/connectivity.proto index 757f48c4..83440463 100644 --- a/protocol/proto/connectivity.proto +++ b/protocol/proto/connectivity.proto @@ -20,11 +20,14 @@ message PlatformSpecificData { } message NativeAndroidData { - int32 major_sdk_version = 1; - int32 minor_sdk_version = 2; - int32 patch_sdk_version = 3; - uint32 api_version = 4; - Screen screen_dimensions = 5; + Screen screen_dimensions = 1; + string android_version = 2; + int32 api_version = 3; + string device_name = 4; + string model_str = 5; + string vendor = 6; + string vendor_2 = 7; + int32 unknown_value_8 = 8; } message NativeIOSData { @@ -38,10 +41,14 @@ message NativeIOSData { message NativeWindowsData { int32 os_version = 1; int32 os_build = 3; - int32 unknown_value_4 = 4; + // https://docs.microsoft.com/en-us/dotnet/api/system.platformid?view=net-6.0 + int32 platform_id = 4; + int32 unknown_value_5 = 5; int32 unknown_value_6 = 6; - int32 unknown_value_7 = 7; - int32 unknown_value_8 = 8; + // https://docs.microsoft.com/en-us/dotnet/api/system.reflection.imagefilemachine?view=net-6.0 + int32 image_file_machine = 7; + // https://docs.microsoft.com/en-us/dotnet/api/system.reflection.portableexecutable.machine?view=net-6.0 + int32 pe_machine = 8; bool unknown_value_10 = 10; } @@ -49,4 +56,6 @@ message Screen { int32 width = 1; int32 height = 2; int32 density = 3; + int32 unknown_value_4 = 4; + int32 unknown_value_5 = 5; } From 2c63ef111a3ba1bda6846e3e641694b4614d9a12 Mon Sep 17 00:00:00 2001 From: SuisChan Date: Tue, 25 Jan 2022 11:56:19 +0100 Subject: [PATCH 123/147] Added linux fields to connectivity.proto --- protocol/proto/connectivity.proto | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/protocol/proto/connectivity.proto b/protocol/proto/connectivity.proto index 83440463..f1623b41 100644 --- a/protocol/proto/connectivity.proto +++ b/protocol/proto/connectivity.proto @@ -16,6 +16,7 @@ message PlatformSpecificData { NativeAndroidData android = 1; NativeIOSData ios = 2; NativeWindowsData windows = 4; + NativeDesktopLinuxData desktop_linux = 5; } } @@ -52,6 +53,13 @@ message NativeWindowsData { bool unknown_value_10 = 10; } +message NativeDesktopLinuxData { + string system_name = 1; // uname -s + string system_release = 2; // -r + string system_version = 3; // -v + string hardware = 4; // -i +} + message Screen { int32 width = 1; int32 height = 2; From 3f95a45b276cfd7059263d2dd77f2558cdfc87b0 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 25 Jan 2022 20:02:41 +0100 Subject: [PATCH 124/147] Parse dates without month or day (fixes #943) --- core/src/date.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/core/src/date.rs b/core/src/date.rs index d7cf09ef..3c78f265 100644 --- a/core/src/date.rs +++ b/core/src/date.rs @@ -53,11 +53,22 @@ impl Date { impl TryFrom<&DateMessage> for Date { type Error = crate::Error; fn try_from(msg: &DateMessage) -> Result { - let date = _Date::from_calendar_date( - msg.get_year(), - (msg.get_month() as u8).try_into()?, - msg.get_day() as u8, - )?; + // Some metadata contains a year, but no month. In that case just set January. + let month = if msg.has_month() { + msg.get_month() as u8 + } else { + 1 + }; + + // Having no day will work, but may be unexpected: it will imply the last day + // of the month before. So prevent that, and just set day 1. + let day = if msg.has_day() { + msg.get_day() as u8 + } else { + 1 + }; + + let date = _Date::from_calendar_date(msg.get_year(), month.try_into()?, day)?; let time = Time::from_hms(msg.get_hour() as u8, msg.get_minute() as u8, 0)?; Ok(Self::from_utc(PrimitiveDateTime::new(date, time))) } From 552d9145f4eed3a27dbcf459a113ff1a8fb0c77a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 25 Jan 2022 20:46:10 +0100 Subject: [PATCH 125/147] Feature-gate passthrough decoder --- Cargo.toml | 2 ++ playback/Cargo.toml | 4 +++- playback/src/decoder/mod.rs | 4 +++- playback/src/player.rs | 23 ++++++++++++++++------- src/main.rs | 17 ++++++++++++----- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3df60b84..7fe13f43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,8 @@ gstreamer-backend = ["librespot-playback/gstreamer-backend"] with-dns-sd = ["librespot-core/with-dns-sd", "librespot-discovery/with-dns-sd"] +passthrough-decoder = ["librespot-playback/passthrough-decoder"] + default = ["rodio-backend"] [package.metadata.deb] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 707d28f9..60304507 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -46,7 +46,7 @@ cpal = { version = "0.13", optional = true } symphonia = { version = "0.4", default-features = false, features = ["mp3", "ogg", "vorbis"] } # Legacy Ogg container decoder for the passthrough decoder -ogg = "0.8" +ogg = { version = "0.8", optional = true } # Dithering rand = { version = "0.8", features = ["small_rng"] } @@ -61,3 +61,5 @@ rodio-backend = ["rodio", "cpal"] rodiojack-backend = ["rodio", "cpal/jack"] sdl-backend = ["sdl2"] gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] + +passthrough-decoder = ["ogg"] diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index 2526da34..f980b680 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -2,7 +2,9 @@ use std::ops::Deref; use thiserror::Error; +#[cfg(feature = "passthrough-decoder")] mod passthrough_decoder; +#[cfg(feature = "passthrough-decoder")] pub use passthrough_decoder::PassthroughDecoder; mod symphonia_decoder; @@ -41,7 +43,7 @@ impl AudioPacket { } } - pub fn oggdata(&self) -> AudioPacketResult<&[u8]> { + pub fn raw(&self) -> AudioPacketResult<&[u8]> { match self { AudioPacket::Raw(d) => Ok(d), AudioPacket::Samples(_) => Err(AudioPacketError::Samples), diff --git a/playback/src/player.rs b/playback/src/player.rs index a52cc01b..a679908e 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -35,13 +35,14 @@ use crate::{ config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, convert::Converter, core::{util::SeqGenerator, Error, Session, SpotifyId}, - decoder::{ - AudioDecoder, AudioPacket, AudioPacketPosition, PassthroughDecoder, SymphoniaDecoder, - }, + decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder}, metadata::audio::{AudioFileFormat, AudioFiles, AudioItem}, mixer::AudioFilter, }; +#[cfg(feature = "passthrough-decoder")] +use crate::decoder::PassthroughDecoder; + use crate::SAMPLES_PER_SECOND; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; @@ -931,9 +932,7 @@ impl PlayerTrackLoader { } }; - let result = if self.config.passthrough { - PassthroughDecoder::new(audio_file, format).map(|x| Box::new(x) as Decoder) - } else { + let mut symphonia_decoder = |audio_file, format| { SymphoniaDecoder::new(audio_file, format).map(|mut decoder| { // For formats other that Vorbis, we'll try getting normalisation data from // ReplayGain metadata fields, if present. @@ -944,12 +943,22 @@ impl PlayerTrackLoader { }) }; + #[cfg(feature = "passthrough-decoder")] + let decoder_type = if self.config.passthrough { + PassthroughDecoder::new(audio_file, format).map(|x| Box::new(x) as Decoder) + } else { + symphonia_decoder(audio_file, format) + }; + + #[cfg(not(feature = "passthrough-decoder"))] + let decoder_type = symphonia_decoder(audio_file, format); + let normalisation_data = normalisation_data.unwrap_or_else(|| { warn!("Unable to get normalisation data, continuing with defaults."); NormalisationData::default() }); - let mut decoder = match result { + let mut decoder = match decoder_type { Ok(decoder) => decoder, Err(e) if is_cached => { warn!( diff --git a/src/main.rs b/src/main.rs index 2919ade7..6f837c02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -227,6 +227,7 @@ fn get_setup() -> Setup { const NORMALISATION_RELEASE: &str = "normalisation-release"; const NORMALISATION_THRESHOLD: &str = "normalisation-threshold"; const ONEVENT: &str = "onevent"; + #[cfg(feature = "passthrough-decoder")] const PASSTHROUGH: &str = "passthrough"; const PASSWORD: &str = "password"; const PROXY: &str = "proxy"; @@ -262,6 +263,7 @@ fn get_setup() -> Setup { const NAME_SHORT: &str = "n"; const DISABLE_DISCOVERY_SHORT: &str = "O"; const ONEVENT_SHORT: &str = "o"; + #[cfg(feature = "passthrough-decoder")] const PASSTHROUGH_SHORT: &str = "P"; const PASSWORD_SHORT: &str = "p"; const EMIT_SINK_EVENTS_SHORT: &str = "Q"; @@ -371,11 +373,6 @@ fn get_setup() -> Setup { EMIT_SINK_EVENTS, "Run PROGRAM set by `--onevent` before the sink is opened and after it is closed.", ) - .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, @@ -568,6 +565,13 @@ fn get_setup() -> Setup { "PORT", ); + #[cfg(feature = "passthrough-decoder")] + opts.optflag( + PASSTHROUGH_SHORT, + PASSTHROUGH, + "Pass a raw stream to the output. Only works with the pipe and subprocess backends.", + ); + let args: Vec<_> = std::env::args_os() .filter_map(|s| match s.into_string() { Ok(valid) => Some(valid), @@ -1505,7 +1509,10 @@ fn get_setup() -> Setup { }, }; + #[cfg(feature = "passthrough-decoder")] let passthrough = opt_present(PASSTHROUGH); + #[cfg(not(feature = "passthrough-decoder"))] + let passthrough = false; PlayerConfig { bitrate, From 44860f4738e07a0b9409ec3b412e3100a498138b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 25 Jan 2022 20:58:39 +0100 Subject: [PATCH 126/147] Remove assertions for what we know works well --- audio/src/range_set.rs | 1 - connect/src/spirc.rs | 3 --- core/src/authentication.rs | 1 - core/src/channel.rs | 1 - core/src/dealer/mod.rs | 1 + 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/audio/src/range_set.rs b/audio/src/range_set.rs index 005a4cda..9c4b0b87 100644 --- a/audio/src/range_set.rs +++ b/audio/src/range_set.rs @@ -229,7 +229,6 @@ impl RangeSet { self.ranges[self_index].end(), other.ranges[other_index].end(), ); - assert!(new_start <= new_end); result.add_range(&Range::new(new_start, new_end - new_start)); if self.ranges[self_index].end() <= other.ranges[other_index].end() { self_index += 1; diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index bea95c57..91256326 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1059,7 +1059,6 @@ impl SpircTask { fn handle_unavailable(&mut self, track_id: SpotifyId) { let unavailables = self.get_track_index_for_spotify_id(&track_id, 0); for &index in unavailables.iter() { - debug_assert_eq!(self.state.get_track()[index].get_gid(), track_id.to_raw()); let mut unplayable_track_ref = TrackRef::new(); unplayable_track_ref.set_gid(self.state.get_track()[index].get_gid().to_vec()); // Misuse context field to flag the track @@ -1320,8 +1319,6 @@ impl SpircTask { .filter(|&(_, track_ref)| track_ref.get_gid() == track_id.to_raw()) .map(|(idx, _)| start_index + idx) .collect(); - // Sanity check - debug_assert!(!index.is_empty()); index } diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 82df5060..1e5cf436 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -114,7 +114,6 @@ impl Credentials { let cipher = Aes192::new(GenericArray::from_slice(&key)); let block_size = ::BlockSize::to_usize(); - assert_eq!(data.len() % block_size, 0); for chunk in data.chunks_exact_mut(block_size) { cipher.decrypt_block(GenericArray::from_mut_slice(chunk)); } diff --git a/core/src/channel.rs b/core/src/channel.rs index 607189a0..c601cd7a 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -173,7 +173,6 @@ impl Stream for Channel { let length = BigEndian::read_u16(data.split_to(2).as_ref()) as usize; if length == 0 { - assert_eq!(data.len(), 0); self.state = ChannelState::Data; } else { let header_id = data.split_to(1).as_ref()[0]; diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index d598e6df..8da0a58c 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -165,6 +165,7 @@ fn split_uri(s: &str) -> Option> { let rest = rest.trim_end_matches(sep); let mut split = rest.split(sep); + #[cfg(debug_assertions)] if rest.is_empty() { assert_eq!(split.next(), Some("")); } From f40fe7de43b5d12ac80ed64e960c86db716966bd Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 25 Jan 2022 21:18:06 +0100 Subject: [PATCH 127/147] Update crates --- Cargo.lock | 233 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 153 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98542dcd..f52d0866 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.52" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" [[package]] name = "arrayvec" @@ -258,14 +258,14 @@ checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" dependencies = [ "glob", "libc", - "libloading 0.7.2", + "libloading 0.7.3", ] [[package]] name = "combine" -version = "4.6.2" +version = "4.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b2f5d0ee456f3928812dfc8c6d9a1d592b98678f6d56db9b0cd2b7bc6c8db5" +checksum = "50b727aacc797f9fc28e355d21f34709ac4fc9adecfe470ad07b8f4464f53062" dependencies = [ "bytes", "memchr", @@ -374,8 +374,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + +[[package]] +name = "darling" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" +dependencies = [ + "darling_core 0.13.1", + "darling_macro 0.13.1", ] [[package]] @@ -388,7 +398,21 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a340f241d2ceed1deb47ae36c4144b2707ec7dd0b649f894cb39bb595986324" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", "syn", ] @@ -398,7 +422,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ - "darling_core", + "darling_core 0.10.2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" +dependencies = [ + "darling_core 0.13.1", "quote", "syn", ] @@ -484,9 +519,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ "instant", ] @@ -623,9 +658,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" dependencies = [ "cfg-if", "libc", @@ -1172,9 +1207,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" dependencies = [ "wasm-bindgen", ] @@ -1193,9 +1228,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.112" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50" [[package]] name = "libgit2-sys" @@ -1221,9 +1256,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" dependencies = [ "cfg-if", "winapi", @@ -1255,9 +1290,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.25.0" +version = "2.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86835d7763ded6bc16b6c0061ec60214da7550dfcd4ef93745f6f0096129676a" +checksum = "17be42160017e0ae993c03bfdab4ecb6f82ce3f8d515bd8da8fdf18d10703663" dependencies = [ "bitflags", "libc", @@ -1269,9 +1304,9 @@ dependencies = [ [[package]] name = "libpulse-simple-binding" -version = "2.24.1" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a22538257c4d522bea6089d6478507f5d2589ea32150e20740aaaaaba44590" +checksum = "7cbf1a1dfd69a48cb60906399fa1d17f1b75029ef51c0789597be792dfd0bcd5" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -1399,7 +1434,7 @@ dependencies = [ "sha-1 0.10.0", "shannon", "thiserror", - "time 0.3.5", + "time 0.3.6", "tokio", "tokio-stream", "tokio-tungstenite", @@ -1607,20 +1642,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" dependencies = [ "jni-sys", - "ndk-sys", + "ndk-sys 0.2.2", "num_enum", "thiserror", ] [[package]] name = "ndk" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" dependencies = [ "bitflags", "jni-sys", - "ndk-sys", + "ndk-sys 0.3.0", "num_enum", "thiserror", ] @@ -1635,22 +1670,22 @@ dependencies = [ "libc", "log", "ndk 0.3.0", - "ndk-macro", - "ndk-sys", + "ndk-macro 0.2.0", + "ndk-sys 0.2.2", ] [[package]] name = "ndk-glue" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420" +checksum = "04c0d14b0858eb9962a5dac30b809b19f19da7e4547d64af2b0bb051d2e55d79" dependencies = [ "lazy_static", "libc", "log", - "ndk 0.4.0", - "ndk-macro", - "ndk-sys", + "ndk 0.6.0", + "ndk-macro 0.3.0", + "ndk-sys 0.3.0", ] [[package]] @@ -1659,19 +1694,41 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" dependencies = [ - "darling", + "darling 0.10.2", "proc-macro-crate 0.1.5", "proc-macro2", "quote", "syn", ] +[[package]] +name = "ndk-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" +dependencies = [ + "darling 0.13.1", + "proc-macro-crate 1.1.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ndk-sys" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + [[package]] name = "nix" version = "0.20.0" @@ -1823,6 +1880,15 @@ dependencies = [ "syn", ] +[[package]] +name = "num_threads" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71a1eb3a36534514077c1e079ada2fb170ef30c47d203aa6916138cf882ecd52" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.27.1" @@ -1834,13 +1900,13 @@ dependencies = [ [[package]] name = "oboe" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1" +checksum = "2463c8f2e19b4e0d0710a21f8e4011501ff28db1c95d7a5482a553b2100502d2" dependencies = [ "jni", - "ndk 0.4.0", - "ndk-glue 0.4.0", + "ndk 0.6.0", + "ndk-glue 0.6.0", "num-derive", "num-traits", "oboe-sys", @@ -1848,9 +1914,9 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc" +checksum = "3370abb7372ed744232c12954d920d1a40f1c4686de9e79e800021ef492294bd" dependencies = [ "cc", ] @@ -1878,9 +1944,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl-probe" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "parking_lot" @@ -2098,9 +2164,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] @@ -2138,9 +2204,9 @@ dependencies = [ [[package]] name = "rand_distr" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964d548f8e7d12e102ef183a0de7e98180c9f8729f555897a857b96e48122d2f" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", "rand", @@ -2376,9 +2442,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" +checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce" dependencies = [ "bitflags", "core-foundation", @@ -2389,9 +2455,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7" dependencies = [ "core-foundation-sys", "libc", @@ -2399,18 +2465,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.133" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" +checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.133" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" +checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d" dependencies = [ "proc-macro2", "quote", @@ -2419,9 +2485,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.74" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" dependencies = [ "itoa 1.0.1", "ryu", @@ -2490,15 +2556,15 @@ checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "socket2" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b" dependencies = [ "libc", "winapi", @@ -2522,6 +2588,12 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strum" version = "0.21.0" @@ -2633,9 +2705,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", @@ -2738,11 +2810,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41effe7cfa8af36f439fac33861b66b049edc6f9a32331e2312660529c1c24ad" +checksum = "c8d54b9298e05179c335de2b9645d061255bcd5155f843b3e328d2cfe0a5b413" dependencies = [ "libc", + "num_threads", ] [[package]] @@ -2995,9 +3068,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "6.0.0" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0c9f8387e118573859ae0e6c6fbdfa41bd1f4fbea451b0b8c5a81a3b8bc9e0" +checksum = "467c706f13b7177c8a138858cbd99c774613eb8e0ff42cf592d65a82f59370c8" dependencies = [ "anyhow", "cfg-if", @@ -3056,9 +3129,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3066,9 +3139,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" dependencies = [ "bumpalo", "lazy_static", @@ -3081,9 +3154,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3091,9 +3164,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" dependencies = [ "proc-macro2", "quote", @@ -3104,15 +3177,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" dependencies = [ "js-sys", "wasm-bindgen", From 31c682453b2a09f7710ec25e350a07be6f7c2d80 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 25 Jan 2022 22:48:27 +0100 Subject: [PATCH 128/147] Prevent man-in-the-middle attacks --- Cargo.lock | 143 +++++++++++++++++++++++++++++-- core/Cargo.toml | 1 + core/src/connection/handshake.rs | 52 ++++++++++- 3 files changed, 187 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f52d0866..59e0d652 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,6 +95,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + [[package]] name = "autocfg" version = "1.0.1" @@ -271,6 +277,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "const-oid" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" + [[package]] name = "core-foundation" version = "0.9.2" @@ -341,6 +353,17 @@ dependencies = [ "libc", ] +[[package]] +name = "crypto-bigint" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03" +dependencies = [ + "generic-array", + "rand_core", + "subtle", +] + [[package]] name = "crypto-common" version = "0.1.1" @@ -438,6 +461,16 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" +dependencies = [ + "const-oid", + "crypto-bigint", +] + [[package]] name = "digest" version = "0.9.0" @@ -1104,7 +1137,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ - "autocfg", + "autocfg 1.0.1", "hashbrown", ] @@ -1219,6 +1252,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] [[package]] name = "lazycell" @@ -1429,6 +1465,7 @@ dependencies = [ "protobuf", "quick-xml", "rand", + "rsa", "serde", "serde_json", "sha-1 0.10.0", @@ -1595,7 +1632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", - "autocfg", + "autocfg 1.0.1", ] [[package]] @@ -1780,12 +1817,30 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ - "autocfg", + "autocfg 1.0.1", "num-integer", "num-traits", "rand", ] +[[package]] +name = "num-bigint-dig" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4547ee5541c18742396ae2c895d0717d0f886d8823b8399cdaf7b07d63ad0480" +dependencies = [ + "autocfg 0.1.7", + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.0" @@ -1812,7 +1867,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ - "autocfg", + "autocfg 1.0.1", "num-traits", ] @@ -1822,7 +1877,7 @@ version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" dependencies = [ - "autocfg", + "autocfg 1.0.1", "num-integer", "num-traits", ] @@ -1833,7 +1888,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ - "autocfg", + "autocfg 1.0.1", "num-bigint", "num-integer", "num-traits", @@ -1845,7 +1900,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "autocfg", + "autocfg 1.0.1", "libm", ] @@ -2026,6 +2081,28 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "116bee8279d783c0cf370efa1a94632f2108e5ef0bb32df31f051647810a4e2c" +dependencies = [ + "der", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447" +dependencies = [ + "der", + "pkcs1", + "spki", + "zeroize", +] + [[package]] name = "pkg-config" version = "0.3.24" @@ -2071,7 +2148,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00ba480ac08d3cfc40dea10fd466fd2c14dee3ea6fc7873bc4079eda2727caf0" dependencies = [ - "autocfg", + "autocfg 1.0.1", "indexmap", ] @@ -2290,6 +2367,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "rsa" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d" +dependencies = [ + "byteorder", + "digest 0.9.0", + "lazy_static", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -2576,6 +2673,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spki" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c01a0c15da1b0b0e1494112e7af814a678fec9bd157881b49beac661e9b6f32" +dependencies = [ + "der", +] + [[package]] name = "stdweb" version = "0.1.3" @@ -3262,3 +3368,24 @@ dependencies = [ "syn", "synstructure", ] + +[[package]] +name = "zeroize" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68d9dcec5f9b43a30d38c49f91dfedfaac384cb8f085faca366c26207dd1619" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e8f13fef10b63c06356d65d416b070798ddabcadc10d3ece0c5be9b3c7eddb" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/core/Cargo.toml b/core/Cargo.toml index ab3be7a7..b65736ef 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -40,6 +40,7 @@ priority-queue = "1.2.1" protobuf = "2" quick-xml = { version = "0.22", features = ["serialize"] } rand = "0.8" +rsa = { version = "0.5", default-features = false, features = ["alloc"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha-1 = "0.10" diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index e686c774..64ed4404 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -4,7 +4,8 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use hmac::{Hmac, Mac}; use protobuf::{self, Message}; use rand::{thread_rng, RngCore}; -use sha1::Sha1; +use rsa::{BigUint, PublicKey}; +use sha1::{Digest, Sha1}; use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio_util::codec::{Decoder, Framed}; @@ -18,10 +19,31 @@ use crate::protocol::keyexchange::{ APResponseMessage, ClientHello, ClientResponsePlaintext, Platform, ProductFlags, }; +const SERVER_KEY: [u8; 256] = [ + 0xac, 0xe0, 0x46, 0x0b, 0xff, 0xc2, 0x30, 0xaf, 0xf4, 0x6b, 0xfe, 0xc3, 0xbf, 0xbf, 0x86, 0x3d, + 0xa1, 0x91, 0xc6, 0xcc, 0x33, 0x6c, 0x93, 0xa1, 0x4f, 0xb3, 0xb0, 0x16, 0x12, 0xac, 0xac, 0x6a, + 0xf1, 0x80, 0xe7, 0xf6, 0x14, 0xd9, 0x42, 0x9d, 0xbe, 0x2e, 0x34, 0x66, 0x43, 0xe3, 0x62, 0xd2, + 0x32, 0x7a, 0x1a, 0x0d, 0x92, 0x3b, 0xae, 0xdd, 0x14, 0x02, 0xb1, 0x81, 0x55, 0x05, 0x61, 0x04, + 0xd5, 0x2c, 0x96, 0xa4, 0x4c, 0x1e, 0xcc, 0x02, 0x4a, 0xd4, 0xb2, 0x0c, 0x00, 0x1f, 0x17, 0xed, + 0xc2, 0x2f, 0xc4, 0x35, 0x21, 0xc8, 0xf0, 0xcb, 0xae, 0xd2, 0xad, 0xd7, 0x2b, 0x0f, 0x9d, 0xb3, + 0xc5, 0x32, 0x1a, 0x2a, 0xfe, 0x59, 0xf3, 0x5a, 0x0d, 0xac, 0x68, 0xf1, 0xfa, 0x62, 0x1e, 0xfb, + 0x2c, 0x8d, 0x0c, 0xb7, 0x39, 0x2d, 0x92, 0x47, 0xe3, 0xd7, 0x35, 0x1a, 0x6d, 0xbd, 0x24, 0xc2, + 0xae, 0x25, 0x5b, 0x88, 0xff, 0xab, 0x73, 0x29, 0x8a, 0x0b, 0xcc, 0xcd, 0x0c, 0x58, 0x67, 0x31, + 0x89, 0xe8, 0xbd, 0x34, 0x80, 0x78, 0x4a, 0x5f, 0xc9, 0x6b, 0x89, 0x9d, 0x95, 0x6b, 0xfc, 0x86, + 0xd7, 0x4f, 0x33, 0xa6, 0x78, 0x17, 0x96, 0xc9, 0xc3, 0x2d, 0x0d, 0x32, 0xa5, 0xab, 0xcd, 0x05, + 0x27, 0xe2, 0xf7, 0x10, 0xa3, 0x96, 0x13, 0xc4, 0x2f, 0x99, 0xc0, 0x27, 0xbf, 0xed, 0x04, 0x9c, + 0x3c, 0x27, 0x58, 0x04, 0xb6, 0xb2, 0x19, 0xf9, 0xc1, 0x2f, 0x02, 0xe9, 0x48, 0x63, 0xec, 0xa1, + 0xb6, 0x42, 0xa0, 0x9d, 0x48, 0x25, 0xf8, 0xb3, 0x9d, 0xd0, 0xe8, 0x6a, 0xf9, 0x48, 0x4d, 0xa1, + 0xc2, 0xba, 0x86, 0x30, 0x42, 0xea, 0x9d, 0xb3, 0x08, 0x6c, 0x19, 0x0e, 0x48, 0xb3, 0x9d, 0x66, + 0xeb, 0x00, 0x06, 0xa2, 0x5a, 0xee, 0xa1, 0x1b, 0x13, 0x87, 0x3c, 0xd7, 0x19, 0xe6, 0x55, 0xbd, +]; + #[derive(Debug, Error)] pub enum HandshakeError { #[error("invalid key length")] InvalidLength, + #[error("server key verification failed")] + VerificationFailed, } pub async fn handshake( @@ -37,7 +59,35 @@ pub async fn handshake( .get_diffie_hellman() .get_gs() .to_owned(); + let remote_signature = message + .get_challenge() + .get_login_crypto_challenge() + .get_diffie_hellman() + .get_gs_signature() + .to_owned(); + // Prevent man-in-the-middle attacks: check server signature + let n = BigUint::from_bytes_be(&SERVER_KEY); + let e = BigUint::new(vec![65537]); + let public_key = rsa::RsaPublicKey::new(n, e).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + HandshakeError::VerificationFailed, + ) + })?; + + let hash = Sha1::digest(&remote_key); + let padding = rsa::padding::PaddingScheme::new_pkcs1v15_sign(Some(rsa::hash::Hash::SHA1)); + public_key + .verify(padding, &hash, &remote_signature) + .map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + HandshakeError::VerificationFailed, + ) + })?; + + // OK to proceed let shared_secret = local_keys.shared_secret(&remote_key); let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator)?; let codec = ApCodec::new(&send_key, &recv_key); From cb1cfddb744f4639c2812b4972a4253baffeaedf Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 26 Jan 2022 22:24:40 +0100 Subject: [PATCH 129/147] Send platform-dependent client token request --- core/src/connection/handshake.rs | 6 ++-- core/src/http_client.rs | 2 +- core/src/spclient.rs | 57 ++++++++++++++++++++++++++----- protocol/proto/connectivity.proto | 12 +++++-- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 64ed4404..680f512e 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -111,11 +111,11 @@ where _ => Platform::PLATFORM_FREEBSD_X86, }, "ios" => match ARCH { - "arm64" => Platform::PLATFORM_IPHONE_ARM64, + "aarch64" => Platform::PLATFORM_IPHONE_ARM64, _ => Platform::PLATFORM_IPHONE_ARM, }, "linux" => match ARCH { - "arm" | "arm64" => Platform::PLATFORM_LINUX_ARM, + "arm" | "aarch64" => Platform::PLATFORM_LINUX_ARM, "blackfin" => Platform::PLATFORM_LINUX_BLACKFIN, "mips" => Platform::PLATFORM_LINUX_MIPS, "sh" => Platform::PLATFORM_LINUX_SH, @@ -128,7 +128,7 @@ where _ => Platform::PLATFORM_OSX_X86, }, "windows" => match ARCH { - "arm" => Platform::PLATFORM_WINDOWS_CE_ARM, + "arm" | "aarch64" => Platform::PLATFORM_WINDOWS_CE_ARM, "x86_64" => Platform::PLATFORM_WIN32_X86_64, _ => Platform::PLATFORM_WIN32_X86, }, diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 7a642444..ed7eac6d 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -86,7 +86,7 @@ impl HttpClient { let spotify_platform = match OS { "android" => "Android/31", - "ios" => "iOS/15.2", + "ios" => "iOS/15.2.1", "macos" => "OSX/0", "windows" => "Win32/0", _ => "Linux/0", diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 7bfa1064..78cccbf0 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -122,14 +122,55 @@ impl SpClient { connectivity_data.set_device_id(self.session().device_id().to_string()); let platform_data = connectivity_data.mut_platform_specific_data(); - let windows_data = platform_data.mut_windows(); - windows_data.set_os_version(10); - windows_data.set_os_build(21370); - windows_data.set_platform_id(2); - windows_data.set_unknown_value_6(9); - windows_data.set_image_file_machine(332); - windows_data.set_pe_machine(34404); - windows_data.set_unknown_value_10(true); + + match std::env::consts::OS { + "windows" => { + let (pe, image_file) = match std::env::consts::ARCH { + "arm" => (448, 452), + "aarch64" => (43620, 452), + "x86_64" => (34404, 34404), + _ => (332, 332), // x86 + }; + + let windows_data = platform_data.mut_desktop_windows(); + windows_data.set_os_version(10); + windows_data.set_os_build(21370); + windows_data.set_platform_id(2); + windows_data.set_unknown_value_6(9); + windows_data.set_image_file_machine(image_file); + windows_data.set_pe_machine(pe); + windows_data.set_unknown_value_10(true); + } + "ios" => { + let ios_data = platform_data.mut_ios(); + ios_data.set_user_interface_idiom(0); + ios_data.set_target_iphone_simulator(false); + ios_data.set_hw_machine("iPhone14,5".to_string()); + ios_data.set_system_version("15.2.1".to_string()); + } + "android" => { + let android_data = platform_data.mut_android(); + android_data.set_android_version("12.0.0_r26".to_string()); + android_data.set_api_version(31); + android_data.set_device_name("Pixel".to_owned()); + android_data.set_model_str("GF5KQ".to_owned()); + android_data.set_vendor("Google".to_owned()); + } + "macos" => { + let macos_data = platform_data.mut_desktop_macos(); + macos_data.set_system_version("Darwin Kernel Version 17.7.0: Fri Oct 30 13:34:27 PDT 2020; root:xnu-4570.71.82.8~1/RELEASE_X86_64".to_string()); + macos_data.set_hw_model("iMac21,1".to_string()); + macos_data.set_compiled_cpu_type(std::env::consts::ARCH.to_string()); + } + _ => { + let linux_data = platform_data.mut_desktop_linux(); + linux_data.set_system_name("Linux".to_string()); + linux_data.set_system_release("5.4.0-56-generic".to_string()); + linux_data + .set_system_version("#62-Ubuntu SMP Mon Nov 23 19:20:19 UTC 2020".to_string()); + linux_data.set_hardware(std::env::consts::ARCH.to_string()); + } + } let body = message.write_to_bytes()?; diff --git a/protocol/proto/connectivity.proto b/protocol/proto/connectivity.proto index f1623b41..ec85e4f9 100644 --- a/protocol/proto/connectivity.proto +++ b/protocol/proto/connectivity.proto @@ -15,7 +15,8 @@ message PlatformSpecificData { oneof data { NativeAndroidData android = 1; NativeIOSData ios = 2; - NativeWindowsData windows = 4; + NativeDesktopMacOSData desktop_macos = 3; + NativeDesktopWindowsData desktop_windows = 4; NativeDesktopLinuxData desktop_linux = 5; } } @@ -32,6 +33,7 @@ message NativeAndroidData { } message NativeIOSData { + // https://developer.apple.com/documentation/uikit/uiuserinterfaceidiom int32 user_interface_idiom = 1; bool target_iphone_simulator = 2; string hw_machine = 3; @@ -39,7 +41,7 @@ message NativeIOSData { string simulator_model_identifier = 5; } -message NativeWindowsData { +message NativeDesktopWindowsData { int32 os_version = 1; int32 os_build = 3; // https://docs.microsoft.com/en-us/dotnet/api/system.platformid?view=net-6.0 @@ -60,6 +62,12 @@ message NativeDesktopLinuxData { string hardware = 4; // -i } +message NativeDesktopMacOSData { + string system_version = 1; + string hw_model = 2; + string compiled_cpu_type = 3; +} + message Screen { int32 width = 1; int32 height = 2; From 827b815da9ffe8bc77f554bda638658a4ecec758 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 26 Jan 2022 22:54:04 +0100 Subject: [PATCH 130/147] Update Rodio and neatly call play/pause --- Cargo.lock | 4 ++-- playback/Cargo.toml | 2 +- playback/src/audio_backend/rodio.rs | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 59e0d652..675bc9e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2350,9 +2350,9 @@ dependencies = [ [[package]] name = "rodio" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d98f5e557b61525057e2bc142c8cd7f0e70d75dc32852309bec440e6e046bf9" +checksum = "ec0939e9f626e6c6f1989adb6226a039c855ca483053f0ee7c98b90e41cf731e" dependencies = [ "cpal", ] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 60304507..cd09641a 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -39,7 +39,7 @@ gstreamer-app = { version = "0.17", optional = true } glib = { version = "0.14", optional = true } # Rodio dependencies -rodio = { version = "0.14", optional = true, default-features = false } +rodio = { version = "0.15", optional = true, default-features = false } cpal = { version = "0.13", optional = true } # Container and audio decoder diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index bbc5de1a..3827a8da 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -186,6 +186,15 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro } impl Sink for RodioSink { + fn start(&mut self) -> SinkResult<()> { + Ok(self.rodio_sink.play()) + } + + fn stop(&mut self) -> SinkResult<()> { + self.rodio_sink.sleep_until_end(); + Ok(self.rodio_sink.pause()) + } + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { let samples = packet .samples() From 9b25669a08dbd5ddf721e5786c1b20b0e984ec28 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 26 Jan 2022 23:05:40 +0100 Subject: [PATCH 131/147] Fix clippy lints --- playback/src/audio_backend/rodio.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 3827a8da..54851d8b 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -187,12 +187,14 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro impl Sink for RodioSink { fn start(&mut self) -> SinkResult<()> { - Ok(self.rodio_sink.play()) + self.rodio_sink.play(); + Ok(()) } fn stop(&mut self) -> SinkResult<()> { self.rodio_sink.sleep_until_end(); - Ok(self.rodio_sink.pause()) + self.rodio_sink.pause(); + Ok(()) } fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { From e64f09fd777d1015f924459419788e8f8765a20a Mon Sep 17 00:00:00 2001 From: Philip Deljanov Date: Tue, 1 Feb 2022 19:02:14 -0500 Subject: [PATCH 132/147] Upgrade to Symphonia v0.5. --- Cargo.lock | 28 +++++++++++------------ playback/Cargo.toml | 2 +- playback/src/decoder/symphonia_decoder.rs | 10 +++++--- playback/src/player.rs | 2 +- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 675bc9e2..55a1b585 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2726,9 +2726,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "symphonia" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e5f38aa07e792f4eebb0faa93cee088ec82c48222dd332897aae1569d9a4b7" +checksum = "eb30457ee7a904dae1e4ace25156dcabaf71e425db318e7885267f09cd8fb648" dependencies = [ "lazy_static", "symphonia-bundle-mp3", @@ -2740,9 +2740,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4d97c4a61ece4651751dddb393ebecb7579169d9e758ae808fe507a5250790" +checksum = "9130cae661447f234b58759d74d23500e9c95697b698589b34196cb0fb488a61" dependencies = [ "bitflags", "lazy_static", @@ -2753,9 +2753,9 @@ dependencies = [ [[package]] name = "symphonia-codec-vorbis" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a29ed6748078effb35a05064a451493a78038918981dc1a76bdf5a2752d441fa" +checksum = "746fc459966b37e277565f9632e5ffd6cbd83d9381152727123f68484cb8f9c4" dependencies = [ "log", "symphonia-core", @@ -2764,9 +2764,9 @@ dependencies = [ [[package]] name = "symphonia-core" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa135e97be0f4a666c31dfe5ef4c75435ba3d355fd6a73d2100aa79b14c104c9" +checksum = "1edcb254d25e02b688b6f8a290a778153fa5f29674ac50773d03e0a16060391d" dependencies = [ "arrayvec", "bitflags", @@ -2777,9 +2777,9 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b2357288a79adfec532cfd86049696cfa5c58efeff83bd51687a528f18a519" +checksum = "00f5b92a2a6370873d9dbe3326dad1bf795b3151efcadca6e5f47d732499a518" dependencies = [ "log", "symphonia-core", @@ -2789,9 +2789,9 @@ dependencies = [ [[package]] name = "symphonia-metadata" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5260599daba18d8fe905ca3eb3b42ba210529a6276886632412cc74984e79b1a" +checksum = "f04ee665c99fd2b919b87261c86a5312e996b720ca142646a163d9583e72bd0e" dependencies = [ "encoding_rs", "lazy_static", @@ -2801,9 +2801,9 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37026c6948ff842e0bf94b4008579cc71ab16ed0ff9ca70a331f60f4f1e1e9" +checksum = "abadfa53359fa437836f2554a0019dd06bfdf742fbb735d0645db3b6c5a763e0" dependencies = [ "symphonia-core", "symphonia-metadata", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index cd09641a..03dd41ae 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -43,7 +43,7 @@ rodio = { version = "0.15", optional = true, default-features = false cpal = { version = "0.13", optional = true } # Container and audio decoder -symphonia = { version = "0.4", default-features = false, features = ["mp3", "ogg", "vorbis"] } +symphonia = { version = "0.5", default-features = false, features = ["mp3", "ogg", "vorbis"] } # Legacy Ogg container decoder for the passthrough decoder ogg = { version = "0.8", optional = true } diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 27cb9e83..e918cec5 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -5,7 +5,7 @@ use symphonia::{ audio::SampleBuffer, codecs::{Decoder, DecoderOptions}, errors::Error, - formats::{FormatReader, SeekMode, SeekTo}, + formats::{FormatOptions, FormatReader, SeekMode, SeekTo}, io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions}, meta::{StandardTagKey, Value}, units::Time, @@ -40,7 +40,11 @@ impl SymphoniaDecoder { }; let mss = MediaSourceStream::new(Box::new(input), mss_opts); - let format_opts = Default::default(); + let format_opts = FormatOptions { + enable_gapless: true, + ..Default::default() + }; + let format: Box = if AudioFiles::is_ogg_vorbis(file_format) { Box::new(OggReader::try_new(mss, &format_opts)?) } else if AudioFiles::is_mp3(file_format) { @@ -188,7 +192,7 @@ impl AudioDecoder for SymphoniaDecoder { } }; - let position_ms = self.ts_to_ms(packet.pts()); + let position_ms = self.ts_to_ms(packet.ts()); let packet_position = AudioPacketPosition { position_ms, skipped, diff --git a/playback/src/player.rs b/playback/src/player.rs index a679908e..cdbeac16 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -2210,7 +2210,7 @@ impl Seek for Subfile { impl MediaSource for Subfile where - R: Read + Seek + Send, + R: Read + Seek + Send + Sync, { fn is_seekable(&self) -> bool { true From ab562cc8d873fb9704782b337471ac5544fe7627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Sun, 13 Feb 2022 22:52:02 +0200 Subject: [PATCH 133/147] Update GStreamer to 0.18 and clean up (#964) * Update GStreamer backend to 0.18 * Don't manually go through all intermediate states when shutting down the GStreamer backend; that happens automatically * Don't initialize GStreamer twice * Use less stringly-typed API for configuring the appsrc * Create our own main context instead of stealing the default one; if the application somewhere else uses the default main context this would otherwise fail in interesting ways * Create GStreamer pipeline more explicitly instead of going via strings for everything * Add an audioresample element before the sink in case the sink doesn't support the sample rate * Remove unnecessary `as_bytes()` call * Use a GStreamer bus sync handler instead of spawning a new thread with a mainloop; it's only used for printing errors or when the end of the stream is reached, which can also be done as well when synchronously handling messages. * Change `expect()` calls to proper error returns wherever possible in GStreamer backend * Store asynchronously reported error in GStreamer backend and return them on next write * Update MSRV to 1.56 --- .github/workflows/test.yml | 6 +- COMPILING.md | 2 +- Cargo.lock | 160 ++++++++++++----------- playback/Cargo.toml | 9 +- playback/src/audio_backend/gstreamer.rs | 162 +++++++++++++----------- 5 files changed, 176 insertions(+), 163 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9535537a..0eb79985 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.53 # MSRV (Minimum supported rust version) + - 1.56 # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -113,7 +113,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - 1.53 # MSRV (Minimum supported rust version) + - 1.56 # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -160,7 +160,7 @@ jobs: os: [ubuntu-latest] target: [armv7-unknown-linux-gnueabihf] toolchain: - - 1.53 # MSRV (Minimum supported rust version) + - 1.56 # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/COMPILING.md b/COMPILING.md index 6f390447..8875076e 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -7,7 +7,7 @@ In order to compile librespot, you will first need to set up a suitable Rust bui ### Install Rust 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.53.* +*Note: The current minimum required Rust version at the time of writing is 1.56.* #### Additional Rust tools - `rustfmt` 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: diff --git a/Cargo.lock b/Cargo.lock index 55a1b585..77fc64f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,12 @@ version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" +[[package]] +name = "array-init" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6945cc5422176fc5e602e590c2878d2c2acd9a4fe20a4baa7c28022521698ec6" + [[package]] name = "arrayvec" version = "0.7.2" @@ -221,9 +227,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b412e83326147c2bb881f8b40edfbf9905b9b8abaebd0e47ca190ba62fda8f0e" +checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" dependencies = [ "smallvec", ] @@ -502,12 +508,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - [[package]] name = "encoding_rs" version = "0.8.30" @@ -733,9 +733,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.14.8" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c515f1e62bf151ef6635f528d05b02c11506de986e43b34a5c920ef0b3796a4" +checksum = "e385b6c17a1add7d0fbc64d38e2e742346d3e8b22e5fa3734e5cdca2be24028d" dependencies = [ "bitflags", "futures-channel", @@ -748,13 +748,14 @@ dependencies = [ "libc", "once_cell", "smallvec", + "thiserror", ] [[package]] name = "glib-macros" -version = "0.14.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" +checksum = "e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1" dependencies = [ "anyhow", "heck", @@ -767,9 +768,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.14.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" +checksum = "0c4f08dd67f74b223fedbbb30e73145b9acd444e67cc4d77d0598659b7eebe7e" dependencies = [ "libc", "system-deps", @@ -783,9 +784,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "gobject-sys" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" +checksum = "6edb1f0b3e4c08e2a0a490d1082ba9e902cdff8ff07091e85c6caec60d17e2ab" dependencies = [ "glib-sys", "libc", @@ -794,9 +795,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.17.4" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a255f142048ba2c4a4dce39106db1965abe355d23f4b5335edea43a553faa4" +checksum = "a54229ced7e44752bff52360549cd412802a4b1a19852b87346625ca9f6d4330" dependencies = [ "bitflags", "cfg-if", @@ -810,6 +811,7 @@ dependencies = [ "num-integer", "num-rational", "once_cell", + "option-operations", "paste", "pretty-hex", "thiserror", @@ -817,9 +819,9 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.17.2" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73b8d33b1bbe9f22d0cf56661a1d2a2c9a0e099ea10e5f1f347be5038f5c043" +checksum = "653b14862e385f6a568a5c54aee830c525277418d765e93cdac1c1b97e25f300" dependencies = [ "bitflags", "futures-core", @@ -834,9 +836,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41865cfb8a5ddfa1161734a0d068dcd4689da852be0910b40484206408cfeafa" +checksum = "c3b401f21d731b3e5de802487f25507fabd34de2dd007d582f440fb1c66a4fbb" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -846,10 +848,41 @@ dependencies = [ ] [[package]] -name = "gstreamer-base" -version = "0.17.2" +name = "gstreamer-audio" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0c1d8c62eb5d08fb80173609f2eea71d385393363146e4e78107facbd67715" +checksum = "75cc407516c2f36576060767491f1134728af6d335a59937f09a61aab7abb72c" +dependencies = [ + "array-init", + "bitflags", + "cfg-if", + "glib", + "gstreamer", + "gstreamer-audio-sys", + "gstreamer-base", + "libc", + "once_cell", +] + +[[package]] +name = "gstreamer-audio-sys" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34258fb53c558c0f41dad194037cbeaabf49d347570df11b8bd1c4897cf7d7c" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-base" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224f35f36582407caf58ded74854526beeecc23d0cf64b8d1c3e00584ed6863f" dependencies = [ "bitflags", "cfg-if", @@ -861,9 +894,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28169a7b58edb93ad8ac766f0fa12dcd36a2af4257a97ee10194c7103baf3e27" +checksum = "a083493c3c340e71fa7c66eebda016e9fafc03eb1b4804cf9b2bad61994b078e" dependencies = [ "glib-sys", "gobject-sys", @@ -874,9 +907,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a81704feeb3e8599913bdd1e738455c2991a01ff4a1780cb62200993e454cc3e" +checksum = "e3517a65d3c2e6f8905b456eba5d53bda158d664863aef960b44f651cb7d33e2" dependencies = [ "glib-sys", "gobject-sys", @@ -936,12 +969,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" @@ -1150,15 +1180,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "itertools" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "0.4.8" @@ -1532,6 +1553,7 @@ dependencies = [ "glib", "gstreamer", "gstreamer-app", + "gstreamer-audio", "jack 0.8.4", "libpulse-binding", "libpulse-simple-binding", @@ -2003,6 +2025,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "option-operations" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95d6113415f41b268f1195907427519769e40ee6f28cbb053795098a2c16f447" +dependencies = [ + "paste", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -2534,7 +2565,7 @@ checksum = "94cb479353c0603785c834e2307440d83d196bf255f204f7f6741358de8d6a2f" dependencies = [ "cfg-if", "libc", - "version-compare 0.1.0", + "version-compare", ] [[package]] @@ -2700,24 +2731,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "strum" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" - -[[package]] -name = "strum_macros" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "subtle" version = "2.4.1" @@ -2834,20 +2847,15 @@ dependencies = [ [[package]] name = "system-deps" -version = "3.2.0" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" +checksum = "ad3a97fdef3daf935d929b3e97e5a6a680cd4622e40c2941ca0875d6566416f8" dependencies = [ - "anyhow", "cfg-expr", "heck", - "itertools", "pkg-config", - "strum", - "strum_macros", - "thiserror", "toml", - "version-compare 0.0.11", + "version-compare", ] [[package]] @@ -3115,12 +3123,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" - [[package]] name = "unicode-width" version = "0.1.9" @@ -3188,12 +3190,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "version-compare" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" - [[package]] name = "version-compare" version = "0.1.0" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 03dd41ae..ba51428e 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -34,9 +34,10 @@ libpulse-binding = { version = "2", optional = true, default-features = f libpulse-simple-binding = { version = "2", optional = true, default-features = false } jack = { version = "0.8", optional = true } sdl2 = { version = "0.35", optional = true } -gstreamer = { version = "0.17", optional = true } -gstreamer-app = { version = "0.17", optional = true } -glib = { version = "0.14", optional = true } +gst = { package = "gstreamer", version = "0.18", optional = true } +gst-app = { package = "gstreamer-app", version = "0.18", optional = true } +gst-audio = { package = "gstreamer-audio", version = "0.18", optional = true } +glib = { version = "0.15", optional = true } # Rodio dependencies rodio = { version = "0.15", optional = true, default-features = false } @@ -60,6 +61,6 @@ jackaudio-backend = ["jack"] rodio-backend = ["rodio", "cpal"] rodiojack-backend = ["rodio", "cpal/jack"] sdl-backend = ["sdl2"] -gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] +gstreamer-backend = ["gst", "gst-app", "gst-audio", "glib"] passthrough-decoder = ["ogg"] diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 0b8b63bc..721c0db8 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,14 +1,11 @@ -use std::{ops::Drop, thread}; - -use gstreamer as gst; -use gstreamer_app as gst_app; - use gst::{ event::{FlushStart, FlushStop}, prelude::*, State, }; -use zerocopy::AsBytes; + +use parking_lot::Mutex; +use std::sync::Arc; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; @@ -16,12 +13,12 @@ use crate::{ config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS, SAMPLE_RATE, }; -#[allow(dead_code)] pub struct GstreamerSink { appsrc: gst_app::AppSrc, bufferpool: gst::BufferPool, pipeline: gst::Pipeline, format: AudioFormat, + async_error: Arc>>, } impl Open for GstreamerSink { @@ -29,80 +26,95 @@ impl Open for GstreamerSink { info!("Using GStreamer sink with format: {:?}", format); gst::init().expect("failed to init GStreamer!"); - // GStreamer calls S24 and S24_3 different from the rest of the world let gst_format = match format { - AudioFormat::S24 => "S24_32".to_string(), - AudioFormat::S24_3 => "S24".to_string(), - _ => format!("{:?}", format), + AudioFormat::F64 => gst_audio::AUDIO_FORMAT_F64, + AudioFormat::F32 => gst_audio::AUDIO_FORMAT_F32, + AudioFormat::S32 => gst_audio::AUDIO_FORMAT_S32, + AudioFormat::S24 => gst_audio::AUDIO_FORMAT_S2432, + AudioFormat::S24_3 => gst_audio::AUDIO_FORMAT_S24, + AudioFormat::S16 => gst_audio::AUDIO_FORMAT_S16, }; + + let gst_info = gst_audio::AudioInfo::builder(gst_format, SAMPLE_RATE, NUM_CHANNELS as u32) + .build() + .expect("Failed to create GStreamer audio format"); + let gst_caps = gst_info.to_caps().expect("Failed to create GStreamer caps"); + let sample_size = format.size(); let gst_bytes = NUM_CHANNELS as usize * 1024 * sample_size; - #[cfg(target_endian = "little")] - const ENDIANNESS: &str = "LE"; - #[cfg(target_endian = "big")] - const ENDIANNESS: &str = "BE"; - - let pipeline_str_preamble = format!( - "appsrc caps=\"audio/x-raw,format={}{},layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ", - gst_format, ENDIANNESS, NUM_CHANNELS, SAMPLE_RATE, gst_bytes - ); - // no need to dither twice; use librespot dithering instead - let pipeline_str_rest = r#" ! audioconvert dithering=none ! autoaudiosink"#; - let pipeline_str: String = match device { - Some(x) => format!("{}{}", pipeline_str_preamble, x), - None => format!("{}{}", pipeline_str_preamble, pipeline_str_rest), - }; - info!("Pipeline: {}", pipeline_str); - - gst::init().unwrap(); - let pipelinee = gst::parse_launch(&*pipeline_str).expect("Couldn't launch pipeline; likely a GStreamer issue or an error in the pipeline string you specified in the 'device' argument to librespot."); - let pipeline = pipelinee - .dynamic_cast::() - .expect("couldn't cast pipeline element at runtime!"); - let bus = pipeline.bus().expect("couldn't get bus from pipeline"); - let mainloop = glib::MainLoop::new(None, false); - let appsrce: gst::Element = pipeline - .by_name("appsrc0") - .expect("couldn't get appsrc from pipeline"); - let appsrc: gst_app::AppSrc = appsrce - .dynamic_cast::() + let pipeline = gst::Pipeline::new(None); + let appsrc = gst::ElementFactory::make("appsrc", None) + .expect("Failed to create GStreamer appsrc element") + .downcast::() .expect("couldn't cast AppSrc element at runtime!"); - let appsrc_caps = appsrc.caps().expect("couldn't get appsrc caps"); + appsrc.set_caps(Some(&gst_caps)); + appsrc.set_max_bytes(gst_bytes as u64); + appsrc.set_block(true); + + let sink = match device { + None => { + // no need to dither twice; use librespot dithering instead + gst::parse_bin_from_description( + "audioconvert dithering=none ! audioresample ! autoaudiosink", + true, + ) + .expect("Failed to create default GStreamer sink") + } + Some(ref x) => gst::parse_bin_from_description(x, true) + .expect("Failed to create custom GStreamer sink"), + }; + pipeline + .add(&appsrc) + .expect("Failed to add GStreamer appsrc to pipeline"); + pipeline + .add(&sink) + .expect("Failed to add GStreamer sink to pipeline"); + appsrc + .link(&sink) + .expect("Failed to link GStreamer source to sink"); + + let bus = pipeline.bus().expect("couldn't get bus from pipeline"); let bufferpool = gst::BufferPool::new(); let mut conf = bufferpool.config(); - conf.set_params(Some(&appsrc_caps), gst_bytes as u32, 0, 0); + conf.set_params(Some(&gst_caps), gst_bytes as u32, 0, 0); bufferpool .set_config(conf) .expect("couldn't configure the buffer pool"); - thread::spawn(move || { - let thread_mainloop = mainloop; - let watch_mainloop = thread_mainloop.clone(); - bus.add_watch(move |_, msg| { - match msg.view() { - gst::MessageView::Eos(_) => { - println!("gst signaled end of stream"); - watch_mainloop.quit(); - } - gst::MessageView::Error(err) => { - println!( - "Error from {:?}: {} ({:?})", - err.src().map(|s| s.path_string()), - err.error(), - err.debug() - ); - watch_mainloop.quit(); - } - _ => (), - }; + let async_error = Arc::new(Mutex::new(None)); + let async_error_clone = async_error.clone(); - glib::Continue(true) - }) - .expect("failed to add bus watch"); - thread_mainloop.run(); + bus.set_sync_handler(move |_bus, msg| { + match msg.view() { + gst::MessageView::Eos(_) => { + println!("gst signaled end of stream"); + + let mut async_error_storage = async_error_clone.lock(); + *async_error_storage = Some(String::from("gst signaled end of stream")); + } + gst::MessageView::Error(err) => { + println!( + "Error from {:?}: {} ({:?})", + err.src().map(|s| s.path_string()), + err.error(), + err.debug() + ); + + let mut async_error_storage = async_error_clone.lock(); + *async_error_storage = Some(format!( + "Error from {:?}: {} ({:?})", + err.src().map(|s| s.path_string()), + err.error(), + err.debug() + )); + } + _ => (), + } + + gst::BusSyncReply::Drop }); pipeline @@ -114,16 +126,18 @@ impl Open for GstreamerSink { bufferpool, pipeline, format, + async_error, } } } impl Sink for GstreamerSink { fn start(&mut self) -> SinkResult<()> { + *self.async_error.lock() = None; self.appsrc.send_event(FlushStop::new(true)); self.bufferpool .set_active(true) - .expect("couldn't activate buffer pool"); + .map_err(|e| SinkError::StateChange(e.to_string()))?; self.pipeline .set_state(State::Playing) .map_err(|e| SinkError::StateChange(e.to_string()))?; @@ -131,13 +145,14 @@ impl Sink for GstreamerSink { } fn stop(&mut self) -> SinkResult<()> { + *self.async_error.lock() = None; self.appsrc.send_event(FlushStart::new()); self.pipeline .set_state(State::Paused) .map_err(|e| SinkError::StateChange(e.to_string()))?; self.bufferpool .set_active(false) - .expect("couldn't deactivate buffer pool"); + .map_err(|e| SinkError::StateChange(e.to_string()))?; Ok(()) } @@ -146,15 +161,16 @@ impl Sink for GstreamerSink { impl Drop for GstreamerSink { fn drop(&mut self) { - // Follow the state transitions documented at: - // https://gstreamer.freedesktop.org/documentation/additional/design/states.html?gi-language=c - let _ = self.pipeline.set_state(State::Ready); let _ = self.pipeline.set_state(State::Null); } } impl SinkAsBytes for GstreamerSink { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { + if let Some(async_error) = &*self.async_error.lock() { + return Err(SinkError::OnWrite(async_error.to_string())); + } + let mut buffer = self .bufferpool .acquire_buffer(None) @@ -163,8 +179,8 @@ impl SinkAsBytes for GstreamerSink { let mutbuf = buffer.make_mut(); mutbuf.set_size(data.len()); mutbuf - .copy_from_slice(0, data.as_bytes()) - .expect("Failed to copy from slice"); + .copy_from_slice(0, data) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; self.appsrc .push_buffer(buffer) From 47f1362453d7b4b99933578c044709251da5918b Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 14 Feb 2022 05:15:19 -0600 Subject: [PATCH 134/147] Port remove unsafe code and catch up with dev (#956) --- CHANGELOG.md | 4 + connect/src/spirc.rs | 6 +- core/src/cache.rs | 17 +- core/src/file_id.rs | 7 +- core/src/spclient.rs | 14 +- core/src/spotify_id.rs | 62 ++++--- examples/playlist_tracks.rs | 12 +- metadata/src/episode.rs | 2 +- metadata/src/playlist/annotation.rs | 2 +- metadata/src/playlist/list.rs | 4 +- metadata/src/track.rs | 2 +- playback/src/config.rs | 2 +- playback/src/convert.rs | 40 ++--- playback/src/dither.rs | 6 +- playback/src/mixer/alsamixer.rs | 15 +- playback/src/player.rs | 266 +++++++++++++++++----------- src/main.rs | 23 +-- src/player_event_handler.rs | 123 +++++++++---- 18 files changed, 366 insertions(+), 241 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0a91a29..6db058bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [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. - [main] `--alsa-mixer-device` and `--alsa-mixer-index` now fallback to the card and index specified in `--device`. +- [core] Removed unsafe code (breaking) +- [playback] Adhere to ReplayGain spec when calculating gain normalisation factor. +- [playback] `alsa`: Use `--volume-range` overrides for softvol controls +- [connect] Don't panic when activating shuffle without previous interaction. ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 91256326..db5ff0c9 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -859,15 +859,15 @@ impl SpircTask { self.state.set_shuffle(update.get_state().get_shuffle()); if self.state.get_shuffle() { let current_index = self.state.get_playing_track_index(); - { - let tracks = self.state.mut_track(); + let tracks = self.state.mut_track(); + if !tracks.is_empty() { tracks.swap(0, current_index as usize); if let Some((_, rest)) = tracks.split_first_mut() { let mut rng = rand::thread_rng(); rest.shuffle(&mut rng); } + self.state.set_playing_track_index(0); } - self.state.set_playing_track_index(0); } else { let context = self.state.get_context_uri(); debug!("{:?}", context); diff --git a/core/src/cache.rs b/core/src/cache.rs index 9b81e943..f4fadc67 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -368,12 +368,17 @@ impl Cache { } 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]); - path.push(&name[2..]); - path - }) + match file.to_base16() { + Ok(name) => self.audio_location.as_ref().map(|location| { + let mut path = location.join(&name[0..2]); + path.push(&name[2..]); + path + }), + Err(e) => { + warn!("{}", e); + None + } + } } pub fn file(&self, file: FileId) -> Option { diff --git a/core/src/file_id.rs b/core/src/file_id.rs index 79969848..5422c428 100644 --- a/core/src/file_id.rs +++ b/core/src/file_id.rs @@ -2,7 +2,7 @@ use std::fmt; use librespot_protocol as protocol; -use crate::spotify_id::to_base16; +use crate::{spotify_id::to_base16, Error}; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct FileId(pub [u8; 20]); @@ -14,7 +14,8 @@ impl FileId { FileId(dst) } - pub fn to_base16(&self) -> String { + #[allow(clippy::wrong_self_convention)] + pub fn to_base16(&self) -> Result { to_base16(&self.0, &mut [0u8; 40]) } } @@ -27,7 +28,7 @@ impl fmt::Debug for FileId { impl fmt::Display for FileId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&self.to_base16()) + f.write_str(&self.to_base16().unwrap_or_default()) } } diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 78cccbf0..37a125ad 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -354,7 +354,7 @@ impl SpClient { } pub async fn get_metadata(&self, scope: &str, id: SpotifyId) -> SpClientResult { - let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()); + let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()?); self.request(&Method::GET, &endpoint, None, None).await } @@ -379,7 +379,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 @@ -392,7 +392,7 @@ impl SpClient { ) -> SpClientResult { let endpoint = format!( "/color-lyrics/v2/track/{}/image/spotify:image:{}", - track_id.to_base62(), + track_id.to_base62()?, image_id ); @@ -416,7 +416,7 @@ impl SpClient { pub async fn get_audio_storage(&self, file_id: FileId) -> SpClientResult { let endpoint = format!( "/storage-resolve/files/audio/interactive/{}", - file_id.to_base16() + file_id.to_base16()? ); self.request(&Method::GET, &endpoint, None, None).await } @@ -459,7 +459,7 @@ impl SpClient { .get_user_attribute(attribute) .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?; - let mut url = template.replace("{id}", &preview_id.to_base16()); + let mut url = template.replace("{id}", &preview_id.to_base16()?); let separator = match url.find('?') { Some(_) => "&", None => "?", @@ -477,7 +477,7 @@ impl SpClient { .get_user_attribute(attribute) .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?; - let url = template.replace("{file_id}", &file_id.to_base16()); + let url = template.replace("{file_id}", &file_id.to_base16()?); self.request_url(url).await } @@ -488,7 +488,7 @@ impl SpClient { .session() .get_user_attribute(attribute) .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?; - let url = template.replace("{file_id}", &image_id.to_base16()); + let url = template.replace("{file_id}", &image_id.to_base16()?); self.request_url(url).await } diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index b8a1448e..7591d427 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -191,7 +191,8 @@ impl SpotifyId { /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32) /// character long `String`. - pub fn to_base16(&self) -> String { + #[allow(clippy::wrong_self_convention)] + pub fn to_base16(&self) -> Result { to_base16(&self.to_raw(), &mut [0u8; Self::SIZE_BASE16]) } @@ -199,7 +200,9 @@ impl SpotifyId { /// character long `String`. /// /// [canonically]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - pub fn to_base62(&self) -> String { + + #[allow(clippy::wrong_self_convention)] + pub fn to_base62(&self) -> Result { let mut dst = [0u8; 22]; let mut i = 0; let n = self.id; @@ -237,14 +240,12 @@ impl SpotifyId { dst.reverse(); - unsafe { - // Safety: We are only dealing with ASCII characters. - String::from_utf8_unchecked(dst.to_vec()) - } + String::from_utf8(dst.to_vec()).map_err(|_| SpotifyIdError::InvalidId.into()) } /// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in /// big-endian order. + #[allow(clippy::wrong_self_convention)] pub fn to_raw(&self) -> [u8; Self::SIZE] { self.id.to_be_bytes() } @@ -257,7 +258,9 @@ impl SpotifyId { /// be encoded as `unknown`. /// /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - pub fn to_uri(&self) -> String { + + #[allow(clippy::wrong_self_convention)] + pub fn to_uri(&self) -> Result { // 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31 // + unknown size item_type. let item_type: &str = self.item_type.into(); @@ -265,21 +268,24 @@ impl SpotifyId { dst.push_str("spotify:"); dst.push_str(item_type); dst.push(':'); - dst.push_str(&self.to_base62()); + let base_62 = self.to_base62()?; + dst.push_str(&base_62); - dst + Ok(dst) } } impl fmt::Debug for SpotifyId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_tuple("SpotifyId").field(&self.to_uri()).finish() + f.debug_tuple("SpotifyId") + .field(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) + .finish() } } impl fmt::Display for SpotifyId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&self.to_uri()) + f.write_str(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) } } @@ -312,16 +318,17 @@ impl NamedSpotifyId { }) } - pub fn to_uri(&self) -> String { + pub fn to_uri(&self) -> Result { 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()); + let base_62 = self.to_base62()?; + dst.push_str(&base_62); - dst + Ok(dst) } pub fn from_spotify_id(id: SpotifyId, username: String) -> Self { @@ -342,14 +349,24 @@ 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()) + .field( + &self + .inner_id + .to_uri() + .unwrap_or_else(|_| "invalid id".into()), + ) .finish() } } impl fmt::Display for NamedSpotifyId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&self.inner_id.to_uri()) + f.write_str( + &self + .inner_id + .to_uri() + .unwrap_or_else(|_| "invalid id".into()), + ) } } @@ -495,7 +512,7 @@ impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId { } } -pub fn to_base16(src: &[u8], buf: &mut [u8]) -> String { +pub fn to_base16(src: &[u8], buf: &mut [u8]) -> Result { let mut i = 0; for v in src { buf[i] = BASE16_DIGITS[(v >> 4) as usize]; @@ -503,10 +520,7 @@ pub fn to_base16(src: &[u8], buf: &mut [u8]) -> String { i += 2; } - unsafe { - // Safety: We are only dealing with ASCII characters. - String::from_utf8_unchecked(buf.to_vec()) - } + String::from_utf8(buf.to_vec()).map_err(|_| SpotifyIdError::InvalidId.into()) } #[cfg(test)] @@ -623,7 +637,7 @@ mod tests { item_type: c.kind, }; - assert_eq!(id.to_base62(), c.base62); + assert_eq!(id.to_base62().unwrap(), c.base62); } } @@ -646,7 +660,7 @@ mod tests { item_type: c.kind, }; - assert_eq!(id.to_base16(), c.base16); + assert_eq!(id.to_base16().unwrap(), c.base16); } } @@ -672,7 +686,7 @@ mod tests { item_type: c.kind, }; - assert_eq!(id.to_uri(), c.uri); + assert_eq!(id.to_uri().unwrap(), c.uri); } } diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 2f53a8a3..fdadc61d 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -19,11 +19,13 @@ async fn main() { } let credentials = Credentials::with_password(&args[1], &args[2]); - let uri_split = args[3].split(':'); - let uri_parts: Vec<&str> = uri_split.collect(); - println!("{}, {}, {}", uri_parts[0], uri_parts[1], uri_parts[2]); - - let plist_uri = SpotifyId::from_base62(uri_parts[2]).unwrap(); + let plist_uri = SpotifyId::from_uri(&args[3]).unwrap_or_else(|_| { + eprintln!( + "PLAYLIST should be a playlist URI such as: \ + \"spotify:playlist:37i9dQZF1DXec50AjHrNTq\"" + ); + exit(1); + }); let session = Session::new(session_config, None); if let Err(e) = session.connect(credentials).await { diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index d04282ec..c5b65f80 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -74,7 +74,7 @@ impl InnerAudioItem for Episode { Ok(AudioItem { id, - spotify_uri: id.to_uri(), + spotify_uri: id.to_uri()?, files: episode.audio, name: episode.name, duration: episode.duration, diff --git a/metadata/src/playlist/annotation.rs b/metadata/src/playlist/annotation.rs index 587f9b39..fd8863cf 100644 --- a/metadata/src/playlist/annotation.rs +++ b/metadata/src/playlist/annotation.rs @@ -52,7 +52,7 @@ impl PlaylistAnnotation { let uri = format!( "hm://playlist-annotate/v1/annotation/user/{}/playlist/{}", username, - playlist_id.to_base62() + playlist_id.to_base62()? ); ::request(session, &uri).await } diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 612ef857..0a359694 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -104,7 +104,7 @@ impl Playlist { let uri = format!( "hm://playlist/user/{}/playlist/{}", username, - playlist_id.to_base62() + playlist_id.to_base62()? ); ::request(session, &uri).await } @@ -152,7 +152,7 @@ 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()); + let uri = format!("hm://playlist/v2/playlist/{}", playlist_id.to_base62()?); ::request(session, &uri).await } diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 4808b3f1..001590f5 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -88,7 +88,7 @@ impl InnerAudioItem for Track { Ok(AudioItem { id, - spotify_uri: id.to_uri(), + spotify_uri: id.to_uri()?, files: track.files, name: track.name, duration: track.duration, diff --git a/playback/src/config.rs b/playback/src/config.rs index 4070a26a..f1276adb 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -130,7 +130,7 @@ pub struct PlayerConfig { pub normalisation: bool, pub normalisation_type: NormalisationType, pub normalisation_method: NormalisationMethod, - pub normalisation_pregain_db: f32, + pub normalisation_pregain_db: f64, pub normalisation_threshold_dbfs: f64, pub normalisation_attack_cf: f64, pub normalisation_release_cf: f64, diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 962ade66..1bc8a88e 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -23,14 +23,15 @@ pub struct Converter { impl Converter { pub fn new(dither_config: Option) -> Self { - if let Some(ref ditherer_builder) = dither_config { - let ditherer = (ditherer_builder)(); - info!("Converting with ditherer: {}", ditherer.name()); - Self { - ditherer: Some(ditherer), + match dither_config { + Some(ditherer_builder) => { + let ditherer = (ditherer_builder)(); + info!("Converting with ditherer: {}", ditherer.name()); + Self { + ditherer: Some(ditherer), + } } - } else { - Self { ditherer: None } + None => Self { ditherer: None }, } } @@ -52,18 +53,15 @@ impl Converter { const SCALE_S16: f64 = 32768.; pub fn scale(&mut self, sample: f64, factor: f64) -> f64 { - let dither = match self.ditherer { - Some(ref mut d) => d.noise(), - None => 0.0, - }; - // From the many float to int conversion methods available, match what // the reference Vorbis implementation uses: sample * 32768 (for 16 bit) - let int_value = sample * factor + dither; // Casting float to integer rounds towards zero by default, i.e. it // truncates, and that generates larger error than rounding to nearest. - int_value.round() + match self.ditherer.as_mut() { + Some(d) => (sample * factor + d.noise()).round(), + None => (sample * factor).round(), + } } // Special case for samples packed in a word of greater bit depth (e.g. @@ -79,11 +77,12 @@ impl Converter { let max = factor - 1.0; if int_value < min { - return min; + min } else if int_value > max { - return max; + max + } else { + int_value } - int_value } pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec { @@ -109,12 +108,7 @@ impl Converter { pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec { samples .iter() - .map(|sample| { - // Not as DRY as calling f32_to_s24 first, but this saves iterating - // over all samples twice. - let int_value = self.clamping_scale(*sample, Self::SCALE_S24) as i32; - i24::from_s24(int_value) - }) + .map(|sample| i24::from_s24(self.clamping_scale(*sample, Self::SCALE_S24) as i32)) .collect() } diff --git a/playback/src/dither.rs b/playback/src/dither.rs index 0f667917..4b8a427c 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -3,7 +3,7 @@ use rand::SeedableRng; use rand_distr::{Distribution, Normal, Triangular, Uniform}; use std::fmt; -const NUM_CHANNELS: usize = 2; +use crate::NUM_CHANNELS; // Dithering lowers digital-to-analog conversion ("requantization") error, // linearizing output, lowering distortion and replacing it with a constant, @@ -102,7 +102,7 @@ impl GaussianDitherer { pub struct HighPassDitherer { active_channel: usize, - previous_noises: [f64; NUM_CHANNELS], + previous_noises: [f64; NUM_CHANNELS as usize], cached_rng: SmallRng, distribution: Uniform, } @@ -111,7 +111,7 @@ impl Ditherer for HighPassDitherer { fn new() -> Self { Self { active_channel: 0, - previous_noises: [0.0; NUM_CHANNELS], + previous_noises: [0.0; NUM_CHANNELS as usize], cached_rng: create_rng(), distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB } diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 55398cb7..c04e6ee8 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -84,7 +84,7 @@ impl Mixer for AlsaMixer { warn!("Alsa rounding error detected, setting maximum dB to {:.2} instead of {:.2}", ZERO_DB.to_db(), max_millibel.to_db()); max_millibel = ZERO_DB; } else { - warn!("Please manually set with `--volume-ctrl` if this is incorrect"); + warn!("Please manually set `--volume-range` if this is incorrect"); } } (min_millibel, max_millibel) @@ -104,12 +104,23 @@ impl Mixer for AlsaMixer { let min_db = min_millibel.to_db() as f64; let max_db = max_millibel.to_db() as f64; - let db_range = f64::abs(max_db - min_db); + let mut db_range = f64::abs(max_db - min_db); // Synchronize the volume control dB range with the mixer control, // unless it was already set with a command line option. if !config.volume_ctrl.range_ok() { + if db_range > 100.0 { + debug!("Alsa mixer reported dB range > 100, which is suspect"); + warn!("Please manually set `--volume-range` if this is incorrect"); + } config.volume_ctrl.set_db_range(db_range); + } else { + let db_range_override = config.volume_ctrl.db_range(); + debug!( + "Alsa dB volume range was detected as {} but overridden as {}", + db_range, db_range_override + ); + db_range = db_range_override; } // For hardware controls with a small range (24 dB or less), diff --git a/playback/src/player.rs b/playback/src/player.rs index cdbeac16..e4002878 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -47,6 +47,7 @@ use crate::SAMPLES_PER_SECOND; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; pub const DB_VOLTAGE_RATIO: f64 = 20.0; +pub const PCM_AT_0DBFS: f64 = 1.0; // Spotify inserts a custom Ogg packet at the start with custom metadata values, that you would // otherwise expect in Vorbis comments. This packet isn't well-formed and players may balk at it. @@ -264,7 +265,6 @@ impl Default for NormalisationData { impl NormalisationData { fn parse_from_ogg(mut file: T) -> io::Result { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; - let newpos = file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?; if newpos != SPOTIFY_NORMALIZATION_HEADER_START_OFFSET { error!( @@ -296,31 +296,62 @@ impl NormalisationData { } let (gain_db, gain_peak) = if config.normalisation_type == NormalisationType::Album { - (data.album_gain_db as f64, data.album_peak as f64) + (data.album_gain_db, data.album_peak) } else { - (data.track_gain_db as f64, data.track_peak as f64) + (data.track_gain_db, data.track_peak) }; - let normalisation_power = gain_db + config.normalisation_pregain_db as f64; - let mut normalisation_factor = db_to_ratio(normalisation_power); + // As per the ReplayGain 1.0 & 2.0 (proposed) spec: + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Clipping_prevention + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Clipping_prevention + let normalisation_factor = if config.normalisation_method == NormalisationMethod::Basic { + // For Basic Normalisation, factor = min(ratio of (ReplayGain + PreGain), 1.0 / peak level). + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Peak_amplitude + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Peak_amplitude + // We then limit that to 1.0 as not to exceed dBFS (0.0 dB). + let factor = f64::min( + db_to_ratio(gain_db + config.normalisation_pregain_db), + PCM_AT_0DBFS / gain_peak, + ); - if normalisation_power + ratio_to_db(gain_peak) > config.normalisation_threshold_dbfs { - let limited_normalisation_factor = - db_to_ratio(config.normalisation_threshold_dbfs as f64) / gain_peak; - let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); + if factor > PCM_AT_0DBFS { + info!( + "Lowering gain by {:.2} dB for the duration of this track to avoid potentially exceeding dBFS.", + ratio_to_db(factor) + ); - if config.normalisation_method == NormalisationMethod::Basic { - warn!("Limiting gain to {:.2} dB for the duration of this track to stay under normalisation threshold.", limited_normalisation_power); - normalisation_factor = limited_normalisation_factor; + PCM_AT_0DBFS } else { + factor + } + } else { + // For Dynamic Normalisation it's up to the player to decide, + // factor = ratio of (ReplayGain + PreGain). + // We then let the dynamic limiter handle gain reduction. + let factor = db_to_ratio(gain_db + config.normalisation_pregain_db); + let threshold_ratio = db_to_ratio(config.normalisation_threshold_dbfs); + + if factor > PCM_AT_0DBFS { + let factor_db = gain_db + config.normalisation_pregain_db; + let limiting_db = factor_db + config.normalisation_threshold_dbfs.abs(); + warn!( - "This track will at its peak be subject to {:.2} dB of dynamic limiting.", - normalisation_power - limited_normalisation_power + "This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at it's peak.", + factor_db, limiting_db + ); + } else if factor > threshold_ratio { + let limiting_db = gain_db + + config.normalisation_pregain_db + + config.normalisation_threshold_dbfs.abs(); + + info!( + "This track may be subject to {:.2} dB of dynamic limiting at it's peak.", + limiting_db ); } - warn!("Please lower pregain to avoid."); - } + factor + }; debug!("Normalisation Data: {:?}", data); debug!( @@ -792,7 +823,16 @@ impl PlayerTrackLoader { position_ms: u32, ) -> Option { let audio = match AudioItem::get_file(&self.session, spotify_id).await { - Ok(audio) => audio, + Ok(audio) => match self.find_available_alternative(audio).await { + Some(audio) => audio, + None => { + warn!( + "<{}> is not available", + spotify_id.to_uri().unwrap_or_default() + ); + return None; + } + }, Err(e) => { error!("Unable to load audio item: {:?}", e); return None; @@ -805,6 +845,7 @@ impl PlayerTrackLoader { ); let is_explicit = audio.is_explicit; + if is_explicit { if let Some(value) = self.session.get_user_attribute("filter-explicit-content") { if &value == "1" { @@ -814,22 +855,15 @@ impl PlayerTrackLoader { } } - let audio = match self.find_available_alternative(audio).await { - Some(audio) => audio, - None => { - error!("<{}> is not available", spotify_id.to_uri()); - return None; - } - }; - if audio.duration < 0 { error!( "Track duration for <{}> cannot be {}", - spotify_id.to_uri(), + spotify_id.to_uri().unwrap_or_default(), audio.duration ); return None; } + let duration_ms = audio.duration as u32; // (Most) podcasts seem to support only 96 kbps Ogg Vorbis, so fall back to it @@ -863,25 +897,23 @@ impl PlayerTrackLoader { ], }; - let entry = formats.iter().find_map(|format| { - if let Some(&file_id) = audio.files.get(format) { - Some((*format, file_id)) - } else { - None - } - }); - - let (format, file_id) = match entry { - Some(t) => t, - None => { - error!("<{}> is not available in any supported format", audio.name); - return None; - } - }; + let (format, file_id) = + match formats + .iter() + .find_map(|format| match audio.files.get(format) { + Some(&file_id) => Some((*format, file_id)), + _ => None, + }) { + Some(t) => t, + None => { + warn!("<{}> is not available in any supported format", audio.name); + return None; + } + }; let bytes_per_second = self.stream_data_rate(format); - // This is only a loop to be able to reload the file if an error occured + // This is only a loop to be able to reload the file if an error occurred // while opening a cached file. loop { let encrypted_file = AudioFile::open(&self.session, file_id, bytes_per_second); @@ -1416,73 +1448,98 @@ impl PlayerInternal { // For the basic normalisation method, a normalisation factor of 1.0 indicates that // there is nothing to normalise (all samples should pass unaltered). For the // dynamic method, there may still be peaks that we want to shave off. - if self.config.normalisation - && !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON - && self.config.normalisation_method == NormalisationMethod::Basic) - { - // zero-cost shorthands - let threshold_db = self.config.normalisation_threshold_dbfs; - let knee_db = self.config.normalisation_knee_db; - let attack_cf = self.config.normalisation_attack_cf; - let release_cf = self.config.normalisation_release_cf; + if self.config.normalisation { + if self.config.normalisation_method == NormalisationMethod::Basic + && normalisation_factor < 1.0 + { + for sample in data.iter_mut() { + *sample *= normalisation_factor; + } + } else if self.config.normalisation_method + == NormalisationMethod::Dynamic + { + // zero-cost shorthands + let threshold_db = self.config.normalisation_threshold_dbfs; + let knee_db = self.config.normalisation_knee_db; + let attack_cf = self.config.normalisation_attack_cf; + let release_cf = self.config.normalisation_release_cf; - for sample in data.iter_mut() { - *sample *= normalisation_factor; // for both the basic and dynamic limiter + for sample in data.iter_mut() { + *sample *= normalisation_factor; - // Feedforward limiter in the log domain - // After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic - // Range Compressor Design—A Tutorial and Analysis. Journal of The Audio - // Engineering Society, 60, 399-408. - if self.config.normalisation_method == NormalisationMethod::Dynamic - { - // steps 1 + 2: half-wave rectification and conversion into dB - let abs_sample_db = ratio_to_db(sample.abs()); + // Feedforward limiter in the log domain + // After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic + // Range Compressor Design—A Tutorial and Analysis. Journal of The Audio + // Engineering Society, 60, 399-408. - // Some tracks have samples that are precisely 0.0, but ratio_to_db(0.0) - // returns -inf and gets the peak detector stuck. - if !abs_sample_db.is_normal() { - continue; - } + // Some tracks have samples that are precisely 0.0. That's silence + // and we know we don't need to limit that, in which we can spare + // the CPU cycles. + // + // Also, calling `ratio_to_db(0.0)` returns `inf` and would get the + // peak detector stuck. Also catch the unlikely case where a sample + // is decoded as `NaN` or some other non-normal value. + let limiter_db = if sample.is_normal() { + // step 1-4: half-wave rectification and conversion into dB + // and gain computer with soft knee and subtractor + let bias_db = ratio_to_db(sample.abs()) - threshold_db; + let knee_boundary_db = bias_db * 2.0; - // step 3: gain computer with soft knee - let biased_sample = abs_sample_db - threshold_db; - let limited_sample = if 2.0 * biased_sample < -knee_db { - abs_sample_db - } else if 2.0 * biased_sample.abs() <= knee_db { - abs_sample_db - - (biased_sample + knee_db / 2.0).powi(2) - / (2.0 * knee_db) + if knee_boundary_db < -knee_db { + 0.0 + } else if knee_boundary_db.abs() <= knee_db { + // The textbook equation: + // ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db)) + // Simplifies to: + // ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db) + // Which in our case further simplifies to: + // (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db) + // because knee_boundary_db is 2.0 * bias_db. + (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db) + } else { + // Textbook: + // ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db. + bias_db + } } else { - threshold_db as f64 + 0.0 }; - // step 4: subtractor - let limiter_input = abs_sample_db - limited_sample; - - // Spare the CPU unless the limiter is active or we are riding a peak. - if !(limiter_input > 0.0 + // Spare the CPU unless (1) the limiter is engaged, (2) we + // were in attack or (3) we were in release, and that attack/ + // release wasn't finished yet. + if limiter_db > 0.0 || self.normalisation_integrator > 0.0 - || self.normalisation_peak > 0.0) + || self.normalisation_peak > 0.0 { - continue; + // step 5: smooth, decoupled peak detector + // Textbook: + // release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db + // Simplifies to: + // release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db + self.normalisation_integrator = f64::max( + limiter_db, + release_cf * self.normalisation_integrator + - release_cf * limiter_db + + limiter_db, + ); + // Textbook: + // attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator + // Simplifies to: + // attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator + self.normalisation_peak = attack_cf + * self.normalisation_peak + - attack_cf * self.normalisation_integrator + + self.normalisation_integrator; + + // step 6: make-up gain applied later (volume attenuation) + // Applying the standard normalisation factor here won't work, + // because there are tracks with peaks as high as 6 dB above + // the default threshold, so that would clip. + + // steps 7-8: conversion into level and multiplication into gain stage + *sample *= db_to_ratio(-self.normalisation_peak); } - - // step 5: smooth, decoupled peak detector - self.normalisation_integrator = f64::max( - limiter_input, - release_cf * self.normalisation_integrator - + (1.0 - release_cf) * limiter_input, - ); - self.normalisation_peak = attack_cf * self.normalisation_peak - + (1.0 - attack_cf) * self.normalisation_integrator; - - // step 6: make-up gain applied later (volume attenuation) - // Applying the standard normalisation factor here won't work, - // because there are tracks with peaks as high as 6 dB above - // the default threshold, so that would clip. - - // steps 7-8: conversion into level and multiplication into gain stage - *sample *= db_to_ratio(-self.normalisation_peak); } } } @@ -1981,15 +2038,8 @@ impl PlayerInternal { } fn send_event(&mut self, event: PlayerEvent) { - let mut index = 0; - while index < self.event_senders.len() { - match self.event_senders[index].send(event.clone()) { - Ok(_) => index += 1, - Err(_) => { - self.event_senders.remove(index); - } - } - } + self.event_senders + .retain(|sender| sender.send(event.clone()).is_ok()); } fn load_track( diff --git a/src/main.rs b/src/main.rs index 6f837c02..f81bd1c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -192,7 +192,7 @@ 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..=10.0; - const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.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; @@ -671,6 +671,7 @@ fn get_setup() -> Setup { let opt = key.trim_start_matches('-'); if index > 0 + && key.starts_with('-') && &args[index - 1] != key && matches.opt_defined(opt) && matches.opt_present(opt) @@ -1306,12 +1307,7 @@ fn get_setup() -> Setup { normalisation_method = 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(|_| { + NormalisationMethod::from_str(method).unwrap_or_else(|_| { invalid_error_msg( NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, @@ -1321,16 +1317,7 @@ fn get_setup() -> Setup { ); 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); @@ -1352,7 +1339,7 @@ fn get_setup() -> Setup { .unwrap_or(player_default_config.normalisation_type); normalisation_pregain_db = opt_str(NORMALISATION_PREGAIN) - .map(|pregain| match pregain.parse::() { + .map(|pregain| match pregain.parse::() { Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value, _ => { let valid_values = &format!( diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index d5e4517b..ef6d195c 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -1,59 +1,116 @@ +use log::info; + use std::{ collections::HashMap, - io, + io::{Error, ErrorKind, Result}, process::{Command, ExitStatus}, }; -use log::info; use tokio::process::{Child as AsyncChild, Command as AsyncCommand}; use librespot::playback::player::{PlayerEvent, SinkStatus}; -pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option> { +pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option> { let mut env_vars = HashMap::new(); match event { PlayerEvent::Changed { old_track_id, new_track_id, - } => { - env_vars.insert("PLAYER_EVENT", "changed".to_string()); - env_vars.insert("OLD_TRACK_ID", old_track_id.to_base62()); - env_vars.insert("TRACK_ID", new_track_id.to_base62()); - } - PlayerEvent::Started { track_id, .. } => { - env_vars.insert("PLAYER_EVENT", "started".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); - } - PlayerEvent::Stopped { track_id, .. } => { - env_vars.insert("PLAYER_EVENT", "stopped".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); - } + } => match old_track_id.to_base62() { + Err(_) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + "PlayerEvent::Changed: Invalid old track id", + ))) + } + Ok(old_id) => match new_track_id.to_base62() { + Err(_) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + "PlayerEvent::Changed: Invalid new track id", + ))) + } + Ok(new_id) => { + env_vars.insert("PLAYER_EVENT", "changed".to_string()); + env_vars.insert("OLD_TRACK_ID", old_id); + env_vars.insert("TRACK_ID", new_id); + } + }, + }, + PlayerEvent::Started { track_id, .. } => match track_id.to_base62() { + Err(_) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + "PlayerEvent::Started: Invalid track id", + ))) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "started".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() { + Err(_) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + "PlayerEvent::Stopped: Invalid track id", + ))) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "stopped".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, PlayerEvent::Playing { track_id, duration_ms, position_ms, .. - } => { - env_vars.insert("PLAYER_EVENT", "playing".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); - env_vars.insert("DURATION_MS", duration_ms.to_string()); - env_vars.insert("POSITION_MS", position_ms.to_string()); - } + } => match track_id.to_base62() { + Err(_) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + "PlayerEvent::Playing: Invalid track id", + ))) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "playing".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("DURATION_MS", duration_ms.to_string()); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, PlayerEvent::Paused { track_id, duration_ms, position_ms, .. - } => { - env_vars.insert("PLAYER_EVENT", "paused".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); - env_vars.insert("DURATION_MS", duration_ms.to_string()); - env_vars.insert("POSITION_MS", position_ms.to_string()); - } - PlayerEvent::Preloading { track_id, .. } => { - env_vars.insert("PLAYER_EVENT", "preloading".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); - } + } => match track_id.to_base62() { + Err(_) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + "PlayerEvent::Paused: Invalid track id", + ))) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "paused".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("DURATION_MS", duration_ms.to_string()); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, + PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() { + Err(_) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + "PlayerEvent::Preloading: Invalid track id", + ))) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "preloading".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, PlayerEvent::VolumeSet { volume } => { env_vars.insert("PLAYER_EVENT", "volume_set".to_string()); env_vars.insert("VOLUME", volume.to_string()); @@ -71,7 +128,7 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option io::Result { +pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) -> Result { let mut env_vars = HashMap::new(); env_vars.insert("PLAYER_EVENT", "sink".to_string()); let sink_status = match sink_status { From 85d6c0c714f2af2af85812bd002c333f22c4d6b4 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 16 Feb 2022 23:08:43 -0600 Subject: [PATCH 135/147] symphonia_decoder tweak * Remove unwrap * Refactor normalisation_data. --- playback/src/decoder/symphonia_decoder.rs | 71 +++++++++++------------ 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index e918cec5..08c7b37c 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -104,38 +104,36 @@ impl SymphoniaDecoder { pub fn normalisation_data(&mut self) -> Option { let mut metadata = self.format.metadata(); + + // Advance to the latest metadata revision. + // None means we hit the latest. loop { - if let Some(_discarded_revision) = metadata.pop() { - // Advance to the latest metadata revision. - continue; - } else { - let revision = metadata.current()?; - let tags = revision.tags(); - - if tags.is_empty() { - // The latest metadata entry in the log is empty. - return None; - } - - let mut data = NormalisationData::default(); - let mut i = 0; - while i < tags.len() { - if let Value::Float(value) = tags[i].value { - #[allow(non_snake_case)] - match tags[i].std_key { - Some(StandardTagKey::ReplayGainAlbumGain) => data.album_gain_db = value, - Some(StandardTagKey::ReplayGainAlbumPeak) => data.album_peak = value, - Some(StandardTagKey::ReplayGainTrackGain) => data.track_gain_db = value, - Some(StandardTagKey::ReplayGainTrackPeak) => data.track_peak = value, - _ => (), - } - } - i += 1; - } - - break Some(data); + if metadata.pop().is_none() { + break; } } + + let tags = metadata.current()?.tags(); + + if tags.is_empty() { + None + } else { + let mut data = NormalisationData::default(); + + for tag in tags { + if let Value::Float(value) = tag.value { + match tag.std_key { + Some(StandardTagKey::ReplayGainAlbumGain) => data.album_gain_db = value, + Some(StandardTagKey::ReplayGainAlbumPeak) => data.album_peak = value, + Some(StandardTagKey::ReplayGainTrackGain) => data.track_gain_db = value, + Some(StandardTagKey::ReplayGainTrackPeak) => data.track_peak = value, + _ => (), + } + } + } + + Some(data) + } } fn ts_to_ms(&self, ts: u64) -> u32 { @@ -200,14 +198,15 @@ impl AudioDecoder for SymphoniaDecoder { match self.decoder.decode(&packet) { Ok(decoded) => { - if self.sample_buffer.is_none() { - let spec = *decoded.spec(); - let duration = decoded.capacity() as u64; - self.sample_buffer - .replace(SampleBuffer::new(duration, spec)); - } + let sample_buffer = match self.sample_buffer.as_mut() { + Some(buffer) => buffer, + None => { + let spec = *decoded.spec(); + let duration = decoded.capacity() as u64; + self.sample_buffer.insert(SampleBuffer::new(duration, spec)) + } + }; - let sample_buffer = self.sample_buffer.as_mut().unwrap(); // guaranteed above sample_buffer.copy_interleaved_ref(decoded); let samples = AudioPacket::Samples(sample_buffer.samples().to_vec()); From 30c960a6cd429914137229f1e3670e590d30f6ad Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 22 Feb 2022 19:26:44 -0600 Subject: [PATCH 136/147] Silence compiler warning The `split` variable in `split_uri` should not be `mut`. --- core/src/dealer/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index 8da0a58c..b4cfec8e 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -163,12 +163,7 @@ fn split_uri(s: &str) -> Option> { }; let rest = rest.trim_end_matches(sep); - let mut split = rest.split(sep); - - #[cfg(debug_assertions)] - if rest.is_empty() { - assert_eq!(split.next(), Some("")); - } + let split = rest.split(sep); Some(iter::once(scheme).chain(split)) } From dc9f822c802f353b19085ddce13bc90df4204399 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 19 Mar 2022 21:15:46 -0500 Subject: [PATCH 137/147] Port #976 --- CHANGELOG.md | 3 ++ playback/src/audio_backend/pulseaudio.rs | 41 +++++++++++++----------- src/main.rs | 37 +++++++++++++++++++++ 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6db058bd..d93b636c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [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. +- [playback] `pulseaudio`: set the PulseAudio name to match librespot's device name via `PULSE_PROP_application.name` environment variable (user set env var value takes precedence). (breaking) +- [playback] `pulseaudio`: set icon to `audio-x-generic` so we get an icon instead of a placeholder via `PULSE_PROP_application.icon_name` environment variable (user set env var value takes precedence). (breaking) +- [playback] `pulseaudio`: set values to: `PULSE_PROP_application.version`, `PULSE_PROP_application.process.binary`, `PULSE_PROP_stream.description`, `PULSE_PROP_media.software` and `PULSE_PROP_media.role` environment variables (user set env var values take precedence). (breaking) ### Fixed - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 7487517f..b92acefa 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -5,11 +5,9 @@ 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::env; use thiserror::Error; -const APP_NAME: &str = "librespot"; -const STREAM_NAME: &str = "Spotify endpoint"; - #[derive(Debug, Error)] enum PulseError { #[error(" Unsupported Pulseaudio Sample Spec, Format {pulse_format:?} ({format:?}), Channels {channels}, Rate {rate}")] @@ -47,13 +45,18 @@ impl From for SinkError { } pub struct PulseAudioSink { - s: Option, + sink: Option, device: Option, + app_name: String, + stream_desc: String, format: AudioFormat, } impl Open for PulseAudioSink { fn open(device: Option, format: AudioFormat) -> Self { + let app_name = env::var("PULSE_PROP_application.name").unwrap_or_default(); + let stream_desc = env::var("PULSE_PROP_stream.description").unwrap_or_default(); + let mut actual_format = format; if actual_format == AudioFormat::F64 { @@ -64,8 +67,10 @@ impl Open for PulseAudioSink { info!("Using PulseAudioSink with format: {:?}", actual_format); Self { - s: None, + sink: None, device, + app_name, + stream_desc, format: actual_format, } } @@ -73,7 +78,7 @@ impl Open for PulseAudioSink { impl Sink for PulseAudioSink { fn start(&mut self) -> SinkResult<()> { - if self.s.is_none() { + if self.sink.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, @@ -84,13 +89,13 @@ impl Sink for PulseAudioSink { _ => unreachable!(), }; - let ss = pulse::sample::Spec { + let sample_spec = pulse::sample::Spec { format: pulse_format, channels: NUM_CHANNELS, rate: SAMPLE_RATE, }; - if !ss.is_valid() { + if !sample_spec.is_valid() { let pulse_error = PulseError::InvalidSampleSpec { pulse_format, format: self.format, @@ -101,30 +106,28 @@ impl Sink for PulseAudioSink { return Err(SinkError::from(pulse_error)); } - let s = Simple::new( + let sink = Simple::new( None, // Use the default server. - APP_NAME, // Our application's name. + &self.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. + &self.stream_desc, // Description of our stream. + &sample_spec, // Our sample format. None, // Use default channel map. None, // Use default buffering attributes. ) .map_err(PulseError::ConnectionRefused)?; - self.s = Some(s); + self.sink = Some(sink); } Ok(()) } fn stop(&mut self) -> SinkResult<()> { - let s = self.s.as_mut().ok_or(PulseError::NotConnected)?; + let sink = self.sink.take().ok_or(PulseError::NotConnected)?; - s.drain().map_err(PulseError::DrainFailure)?; - - self.s = None; + sink.drain().map_err(PulseError::DrainFailure)?; Ok(()) } @@ -133,9 +136,9 @@ impl Sink for PulseAudioSink { impl SinkAsBytes for PulseAudioSink { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { - let s = self.s.as_mut().ok_or(PulseError::NotConnected)?; + let sink = self.sink.as_mut().ok_or(PulseError::NotConnected)?; - s.write(data).map_err(PulseError::OnWrite)?; + sink.write(data).map_err(PulseError::OnWrite)?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index f81bd1c0..8d77bc07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1143,6 +1143,43 @@ fn get_setup() -> Setup { exit(1); } + #[cfg(feature = "pulseaudio-backend")] + { + if env::var("PULSE_PROP_application.name").is_err() { + let pulseaudio_name = if name != connect_default_config.name { + format!("{} - {}", connect_default_config.name, name) + } else { + name.clone() + }; + + env::set_var("PULSE_PROP_application.name", pulseaudio_name); + } + + if env::var("PULSE_PROP_application.version").is_err() { + env::set_var("PULSE_PROP_application.version", version::SEMVER); + } + + if env::var("PULSE_PROP_application.icon_name").is_err() { + env::set_var("PULSE_PROP_application.icon_name", "audio-x-generic"); + } + + if env::var("PULSE_PROP_application.process.binary").is_err() { + env::set_var("PULSE_PROP_application.process.binary", "librespot"); + } + + if env::var("PULSE_PROP_stream.description").is_err() { + env::set_var("PULSE_PROP_stream.description", "Spotify Connect endpoint"); + } + + if env::var("PULSE_PROP_media.software").is_err() { + env::set_var("PULSE_PROP_media.software", "Spotify"); + } + + if env::var("PULSE_PROP_media.role").is_err() { + env::set_var("PULSE_PROP_media.role", "music"); + } + } + let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { let volume = match initial_volume.parse::() { From d887d58251c708e742a6392fb3ee025c18cf2c9e Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 19 Mar 2022 22:12:24 -0500 Subject: [PATCH 138/147] Fix clippy warnings --- core/src/spclient.rs | 2 +- discovery/src/lib.rs | 39 ++++++++++++------------------ playback/src/audio_backend/alsa.rs | 2 +- src/main.rs | 2 +- 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 37a125ad..820b2182 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -257,7 +257,7 @@ impl SpClient { let mut tries: usize = 0; let mut last_response; - let body = body.unwrap_or_else(String::new); + let body = body.unwrap_or_default(); loop { tries += 1; diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index b4e95737..02686e3e 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -16,7 +16,6 @@ use std::{ task::{Context, Poll}, }; -use cfg_if::cfg_if; use futures_core::Stream; use thiserror::Error; @@ -117,29 +116,23 @@ impl Builder { let name = self.server_config.name.clone().into_owned(); let server = DiscoveryServer::new(self.server_config, &mut port)??; - let svc; + #[cfg(feature = "with-dns-sd")] + let svc = dns_sd::DNSService::register( + Some(name.as_ref()), + "_spotify-connect._tcp", + None, + None, + port, + &["VERSION=1.0", "CPath=/"], + )?; - cfg_if! { - if #[cfg(feature = "with-dns-sd")] { - svc = dns_sd::DNSService::register( - Some(name.as_ref()), - "_spotify-connect._tcp", - None, - None, - port, - &["VERSION=1.0", "CPath=/"], - )?; - - } else { - let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; - svc = responder.register( - "_spotify-connect._tcp".to_owned(), - name, - port, - &["VERSION=1.0", "CPath=/"], - ) - } - }; + #[cfg(not(feature = "with-dns-sd"))] + let svc = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?.register( + "_spotify-connect._tcp".to_owned(), + name, + port, + &["VERSION=1.0", "CPath=/"], + ); Ok(Discovery { server, _svc: svc }) } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 16aa420d..c639228c 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -144,7 +144,7 @@ fn list_compatible_devices() -> SinkResult<()> { println!( "\tDescription:\n\n\t\t{}\n", - a.desc.unwrap_or_default().replace("\n", "\n\t\t") + a.desc.unwrap_or_default().replace('\n', "\n\t\t") ); println!( diff --git a/src/main.rs b/src/main.rs index 8d77bc07..a194ec0a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -596,7 +596,7 @@ fn get_setup() -> Setup { let stripped_env_key = |k: &str| { k.trim_start_matches("LIBRESPOT_") - .replace("_", "-") + .replace('_', "-") .to_lowercase() }; From 1290ee9925aa9b5df7699daaf5d745c5fef90565 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 7 Apr 2022 22:32:43 +0200 Subject: [PATCH 139/147] Fix clippy warnings --- audio/src/fetch/receive.rs | 2 +- playback/src/mixer/alsamixer.rs | 2 +- playback/src/mixer/mappings.rs | 4 ++-- playback/src/mixer/softmixer.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 5d19722b..af12810c 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -139,7 +139,7 @@ enum ControlFlow { } impl AudioFileFetch { - fn is_download_streaming(&mut self) -> bool { + fn is_download_streaming(&self) -> bool { self.shared.download_streaming.load(Ordering::Acquire) } diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index c04e6ee8..aff441d1 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -191,7 +191,7 @@ impl Mixer for AlsaMixer { mapped_volume = LogMapping::linear_to_mapped(mapped_volume, self.db_range); } - self.config.volume_ctrl.from_mapped(mapped_volume) + self.config.volume_ctrl.as_unmapped(mapped_volume) } fn set_volume(&self, volume: u16) { diff --git a/playback/src/mixer/mappings.rs b/playback/src/mixer/mappings.rs index 04cef439..736b3c3f 100644 --- a/playback/src/mixer/mappings.rs +++ b/playback/src/mixer/mappings.rs @@ -3,7 +3,7 @@ use crate::player::db_to_ratio; pub trait MappedCtrl { fn to_mapped(&self, volume: u16) -> f64; - fn from_mapped(&self, mapped_volume: f64) -> u16; + fn as_unmapped(&self, mapped_volume: f64) -> u16; fn db_range(&self) -> f64; fn set_db_range(&mut self, new_db_range: f64); @@ -49,7 +49,7 @@ impl MappedCtrl for VolumeCtrl { mapped_volume } - fn from_mapped(&self, mapped_volume: f64) -> u16 { + fn as_unmapped(&self, mapped_volume: f64) -> u16 { // More than just an optimization, this ensures that zero mapped volume // is unmapped to non-negative real numbers (otherwise the log and cubic // equations would respectively return -inf and -1/9.) diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index cefc2de5..b0d94a6e 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -26,7 +26,7 @@ impl Mixer for SoftMixer { fn volume(&self) -> u16 { let mapped_volume = f64::from_bits(self.volume.load(Ordering::Relaxed)); - self.volume_ctrl.from_mapped(mapped_volume) + self.volume_ctrl.as_unmapped(mapped_volume) } fn set_volume(&self, volume: u16) { From 0c05aa2effbae888bdf669e15fa49dd47a9b69b8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 7 Apr 2022 22:51:08 +0200 Subject: [PATCH 140/147] Update crates --- Cargo.lock | 815 ++++++++++++++++++++++++++--------------------------- 1 file changed, 407 insertions(+), 408 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 77fc64f7..fd96e5f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,7 +48,19 @@ dependencies = [ "alsa-sys", "bitflags", "libc", - "nix", + "nix 0.20.0", +] + +[[package]] +name = "alsa" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5915f52fe2cf65e83924d037b6c5290b7cee097c6b5c8700746e6168a343fd6b" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix 0.23.1", ] [[package]] @@ -63,9 +75,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.53" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" [[package]] name = "array-init" @@ -81,9 +93,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-trait" -version = "0.1.52" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" dependencies = [ "proc-macro2", "quote", @@ -103,21 +115,24 @@ dependencies = [ [[package]] name = "autocfg" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" +checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" dependencies = [ "addr2line", "cc", @@ -161,18 +176,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" -version = "0.9.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ "generic-array", ] @@ -185,9 +191,9 @@ checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "bytemuck" -version = "1.7.3" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" +checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" [[package]] name = "byteorder" @@ -203,9 +209,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.72" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" dependencies = [ "jobserver", ] @@ -227,9 +233,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.9.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" +checksum = "5e068cb2806bbc15b439846dc16c5f89f8599f2c3e4d73d4449d38f9b2f0b6c5" dependencies = [ "smallvec", ] @@ -264,9 +270,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" +checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" dependencies = [ "glob", "libc", @@ -291,9 +297,9 @@ checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", @@ -317,33 +323,33 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" +checksum = "ca4679a59dbd8c15f064c012dfe8c1163b9453224238b59bb9328c142b8b248b" dependencies = [ "bindgen", ] [[package]] name = "cpal" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" +checksum = "74117836a5124f3629e4b474eed03e479abaf98988b4bb317e29f08cfe0e4116" dependencies = [ - "alsa", + "alsa 0.6.0", "core-foundation-sys", "coreaudio-rs", - "jack 0.7.3", + "jack", "jni", "js-sys", "lazy_static", "libc", "mach", - "ndk 0.3.0", - "ndk-glue 0.3.0", - "nix", + "ndk", + "ndk-glue", + "nix 0.23.1", "oboe", - "parking_lot", + "parking_lot 0.11.2", "stdweb", "thiserror", "web-sys", @@ -352,9 +358,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" dependencies = [ "libc", ] @@ -372,11 +378,12 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ "generic-array", + "typenum", ] [[package]] @@ -399,70 +406,35 @@ dependencies = [ [[package]] name = "darling" -version = "0.10.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core 0.10.2", - "darling_macro 0.10.2", -] - -[[package]] -name = "darling" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" -dependencies = [ - "darling_core 0.13.1", - "darling_macro 0.13.1", + "darling_core", + "darling_macro", ] [[package]] name = "darling_core" -version = "0.10.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.9.3", - "syn", -] - -[[package]] -name = "darling_core" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a340f241d2ceed1deb47ae36c4144b2707ec7dd0b649f894cb39bb595986324" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", + "strsim", "syn", ] [[package]] name = "darling_macro" -version = "0.10.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core 0.10.2", - "quote", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" -dependencies = [ - "darling_core 0.13.1", + "darling_core", "quote", "syn", ] @@ -488,13 +460,12 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ - "block-buffer 0.10.0", + "block-buffer", "crypto-common", - "generic-array", "subtle", ] @@ -510,9 +481,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" dependencies = [ "cfg-if", ] @@ -583,9 +554,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -598,9 +569,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -608,15 +579,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -625,15 +596,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-macro" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", @@ -642,21 +613,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-channel", "futures-core", @@ -691,13 +662,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.10.2+wasi-snapshot-preview1", ] [[package]] @@ -720,9 +691,9 @@ checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" [[package]] name = "git2" -version = "0.13.25" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" +checksum = "3826a6e0e2215d7a41c2bfc7c9244123969273f3476b939a226aac0ab56e9e3c" dependencies = [ "bitflags", "libc", @@ -733,9 +704,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.15.4" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e385b6c17a1add7d0fbc64d38e2e742346d3e8b22e5fa3734e5cdca2be24028d" +checksum = "a826fad715b57834920839d7a594c3b5e416358c7d790bdaba847a40d7c1d96d" dependencies = [ "bitflags", "futures-channel", @@ -753,13 +724,13 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.15.3" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1" +checksum = "dac4d47c544af67747652ab1865ace0ffa1155709723ac4f32e97587dd4735b2" dependencies = [ "anyhow", "heck", - "proc-macro-crate 1.1.0", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -768,9 +739,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.15.4" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4f08dd67f74b223fedbbb30e73145b9acd444e67cc4d77d0598659b7eebe7e" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" dependencies = [ "libc", "system-deps", @@ -784,9 +755,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "gobject-sys" -version = "0.15.1" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb1f0b3e4c08e2a0a490d1082ba9e902cdff8ff07091e85c6caec60d17e2ab" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" dependencies = [ "glib-sys", "libc", @@ -795,9 +766,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.18.3" +version = "0.18.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a54229ced7e44752bff52360549cd412802a4b1a19852b87346625ca9f6d4330" +checksum = "cd58af6f8b268fc335122a3ccc66efa0cd56584948f49a37e5feef0b89dfc29b" dependencies = [ "bitflags", "cfg-if", @@ -819,9 +790,9 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.18.0" +version = "0.18.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653b14862e385f6a568a5c54aee830c525277418d765e93cdac1c1b97e25f300" +checksum = "664adf6abc6546c1ad54492a067dcbc605032c9c789ce8f6f78cb9ddeef4b684" dependencies = [ "bitflags", "futures-core", @@ -849,9 +820,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.18.0" +version = "0.18.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cc407516c2f36576060767491f1134728af6d335a59937f09a61aab7abb72c" +checksum = "9ceb43e669be4c33c38b273fd4ca0511c0a7748987835233c529fc3c805c807e" dependencies = [ "array-init", "bitflags", @@ -919,9 +890,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ "bytes", "fnv", @@ -932,7 +903,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.7.1", "tracing", ] @@ -944,9 +915,9 @@ checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "headers" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c4eb0471fcb85846d8b0690695ef354f9afb11cb03cac2e1d7c9253351afb0" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" dependencies = [ "base64", "bitflags", @@ -955,7 +926,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1 0.9.8", + "sha-1", ] [[package]] @@ -990,11 +961,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddca131f3e7f2ce2df364b57949a9d47915cfbd35e46cfee355ccebbf794d6a2" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.1", + "digest 0.10.3", ] [[package]] @@ -1016,7 +987,7 @@ checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ "bytes", "fnv", - "itoa 1.0.1", + "itoa", ] [[package]] @@ -1032,9 +1003,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" [[package]] name = "httpdate" @@ -1050,9 +1021,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.16" +version = "0.14.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" dependencies = [ "bytes", "futures-channel", @@ -1063,7 +1034,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 0.4.8", + "itoa", "pin-project-lite", "socket2", "tokio", @@ -1117,10 +1088,10 @@ dependencies = [ "http", "hyper", "log", - "rustls 0.20.2", + "rustls 0.20.4", "rustls-native-certs 0.6.1", "tokio", - "tokio-rustls 0.23.2", + "tokio-rustls 0.23.3", ] [[package]] @@ -1163,11 +1134,11 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "hashbrown", ] @@ -1180,31 +1151,12 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" -[[package]] -name = "jack" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d79b205ea723e478eb31a91dcdda100912c69cc32992eb7ba26ec0bbae7bebe4" -dependencies = [ - "bitflags", - "jack-sys", - "lazy_static", - "libc", - "log", -] - [[package]] name = "jack" version = "0.8.4" @@ -1261,9 +1213,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.56" +version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" dependencies = [ "wasm-bindgen", ] @@ -1285,15 +1237,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0005d08a8f7b65fb8073cb697aa0b12b631ed251ce73d862ce50eeb52ce3b50" +checksum = "ec647867e2bf0772e28c8bcde4f0d19a9216916e890543b5a03ed8ef27b8f259" [[package]] name = "libgit2-sys" -version = "0.12.26+1.3.0" +version = "0.13.2+1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" +checksum = "3a42de9a51a5c12e00fc0e4ca6bc2ea43582fc6418488e8f615e905d886f258b" dependencies = [ "cc", "libc", @@ -1323,9 +1275,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" +checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" [[package]] name = "libmdns" @@ -1410,7 +1362,7 @@ dependencies = [ "librespot-protocol", "log", "rpassword", - "sha-1 0.10.0", + "sha-1", "thiserror", "tokio", "url", @@ -1428,7 +1380,7 @@ dependencies = [ "hyper", "librespot-core", "log", - "parking_lot", + "parking_lot 0.11.2", "tempfile", "thiserror", "tokio", @@ -1480,7 +1432,7 @@ dependencies = [ "num-integer", "num-traits", "once_cell", - "parking_lot", + "parking_lot 0.11.2", "pbkdf2", "priority-queue", "protobuf", @@ -1489,14 +1441,14 @@ dependencies = [ "rsa", "serde", "serde_json", - "sha-1 0.10.0", + "sha-1", "shannon", "thiserror", - "time 0.3.6", + "time 0.3.9", "tokio", "tokio-stream", "tokio-tungstenite", - "tokio-util", + "tokio-util 0.6.9", "url", "uuid", "vergen", @@ -1522,7 +1474,7 @@ dependencies = [ "log", "rand", "serde_json", - "sha-1 0.10.0", + "sha-1", "thiserror", "tokio", ] @@ -1546,7 +1498,7 @@ dependencies = [ name = "librespot-playback" version = "0.3.1" dependencies = [ - "alsa", + "alsa 0.5.0", "byteorder", "cpal", "futures-util", @@ -1554,7 +1506,7 @@ dependencies = [ "gstreamer", "gstreamer-app", "gstreamer-audio", - "jack 0.8.4", + "jack", "libpulse-binding", "libpulse-simple-binding", "librespot-audio", @@ -1562,7 +1514,7 @@ dependencies = [ "librespot-metadata", "log", "ogg", - "parking_lot", + "parking_lot 0.11.2", "portaudio-rs", "rand", "rand_distr", @@ -1586,9 +1538,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +checksum = "6f35facd4a5673cb5a48822be2be1d4236c1c99cb4113cab7061ac720d5bf859" dependencies = [ "cc", "libc", @@ -1598,18 +1550,19 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ + "autocfg 1.1.0", "scopeguard", ] [[package]] name = "log" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" dependencies = [ "cfg-if", ] @@ -1641,6 +1594,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "mime" version = "0.3.16" @@ -1654,19 +1616,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", - "autocfg 1.0.1", + "autocfg 1.1.0", ] [[package]] name = "mio" -version = "0.7.14" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" dependencies = [ "libc", "log", "miow", "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", "winapi", ] @@ -1694,18 +1657,6 @@ dependencies = [ "serde", ] -[[package]] -name = "ndk" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" -dependencies = [ - "jni-sys", - "ndk-sys 0.2.2", - "num_enum", - "thiserror", -] - [[package]] name = "ndk" version = "0.6.0" @@ -1714,50 +1665,30 @@ checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" dependencies = [ "bitflags", "jni-sys", - "ndk-sys 0.3.0", + "ndk-sys", "num_enum", "thiserror", ] [[package]] -name = "ndk-glue" -version = "0.3.0" +name = "ndk-context" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" +checksum = "4e3c5cc68637e21fe8f077f6a1c9e0b9ca495bb74895226b476310f613325884" + +[[package]] +name = "ndk-glue" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9ffb7443daba48349d545028777ca98853b018b4c16624aa01223bc29e078da" dependencies = [ "lazy_static", "libc", "log", - "ndk 0.3.0", - "ndk-macro 0.2.0", - "ndk-sys 0.2.2", -] - -[[package]] -name = "ndk-glue" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c0d14b0858eb9962a5dac30b809b19f19da7e4547d64af2b0bb051d2e55d79" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.6.0", - "ndk-macro 0.3.0", - "ndk-sys 0.3.0", -] - -[[package]] -name = "ndk-macro" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" -dependencies = [ - "darling 0.10.2", - "proc-macro-crate 0.1.5", - "proc-macro2", - "quote", - "syn", + "ndk", + "ndk-context", + "ndk-macro", + "ndk-sys", ] [[package]] @@ -1766,19 +1697,13 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ - "darling 0.13.1", - "proc-macro-crate 1.1.0", + "darling", + "proc-macro-crate", "proc-macro2", "quote", "syn", ] -[[package]] -name = "ndk-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" - [[package]] name = "ndk-sys" version = "0.3.0" @@ -1800,6 +1725,19 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "5.1.2" @@ -1812,9 +1750,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi", ] @@ -1839,7 +1777,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-integer", "num-traits", "rand", @@ -1851,7 +1789,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4547ee5541c18742396ae2c895d0717d0f886d8823b8399cdaf7b07d63ad0480" dependencies = [ - "autocfg 0.1.7", + "autocfg 0.1.8", "byteorder", "lazy_static", "libm", @@ -1889,7 +1827,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-traits", ] @@ -1899,7 +1837,7 @@ version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-integer", "num-traits", ] @@ -1910,7 +1848,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-bigint", "num-integer", "num-traits", @@ -1922,7 +1860,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "libm", ] @@ -1938,20 +1876,20 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate 1.1.0", + "proc-macro-crate", "proc-macro2", "quote", "syn", @@ -1959,9 +1897,9 @@ dependencies = [ [[package]] name = "num_threads" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71a1eb3a36534514077c1e079ada2fb170ef30c47d203aa6916138cf882ecd52" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" dependencies = [ "libc", ] @@ -1982,8 +1920,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2463c8f2e19b4e0d0710a21f8e4011501ff28db1c95d7a5482a553b2100502d2" dependencies = [ "jni", - "ndk 0.6.0", - "ndk-glue 0.6.0", + "ndk", + "ndk-glue", "num-derive", "num-traits", "oboe-sys", @@ -2009,9 +1947,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" [[package]] name = "opaque-debug" @@ -2042,7 +1980,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.2", ] [[package]] @@ -2063,18 +2011,31 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.6" +name = "parking_lot_core" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" +checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "paste" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" [[package]] name = "pbkdf2" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4628cc3cf953b82edcd3c1388c5715401420ce5524fedbab426bd5aba017434" +checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" dependencies = [ - "digest 0.10.1", + "digest 0.10.3", "hmac", ] @@ -2136,9 +2097,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "portaudio-rs" @@ -2179,24 +2140,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00ba480ac08d3cfc40dea10fd466fd2c14dee3ea6fc7873bc4079eda2727caf0" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "indexmap", ] [[package]] name = "proc-macro-crate" -version = "0.1.5" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - -[[package]] -name = "proc-macro-crate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ "thiserror", "toml", @@ -2228,33 +2180,33 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" dependencies = [ "unicode-xid", ] [[package]] name = "protobuf" -version = "2.25.2" +version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754" +checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" [[package]] name = "protobuf-codegen" -version = "2.25.2" +version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df8c98c08bd4d6653c2dbae00bd68c1d1d82a360265a5b0bbc73d48c63cb853" +checksum = "aec1632b7c8f2e620343439a7dfd1f3c47b18906c4be58982079911482b5d707" dependencies = [ "protobuf", ] [[package]] name = "protobuf-codegen-pure" -version = "2.25.2" +version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "394a73e2a819405364df8d30042c0f1174737a763e0170497ec9d36f8a2ea8f7" +checksum = "9f8122fdb18e55190c796b088a16bdb70cd7acdcd48f7a8b796b58c62e532cc6" dependencies = [ "protobuf", "protobuf-codegen", @@ -2272,23 +2224,22 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", - "rand_hc", ] [[package]] @@ -2320,29 +2271,20 @@ dependencies = [ "rand", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", -] - [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" dependencies = [ "aho-corasick", "memchr", @@ -2445,9 +2387,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.2" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" +checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" dependencies = [ "log", "ring", @@ -2547,9 +2489,9 @@ dependencies = [ [[package]] name = "sdl2" -version = "0.35.1" +version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f035f8e87735fa3a8437292be49fe6056450f7cbb13c230b4bcd1bdd7279421f" +checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" dependencies = [ "bitflags", "lazy_static", @@ -2559,9 +2501,9 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.35.1" +version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94cb479353c0603785c834e2307440d83d196bf255f204f7f6741358de8d6a2f" +checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" dependencies = [ "cfg-if", "libc", @@ -2570,9 +2512,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags", "core-foundation", @@ -2583,9 +2525,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys", "libc", @@ -2593,18 +2535,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.135" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.135" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", @@ -2613,28 +2555,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" dependencies = [ - "itoa 1.0.1", + "itoa", "ryu", "serde", ] -[[package]] -name = "sha-1" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha-1" version = "0.10.0" @@ -2643,7 +2572,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.1", + "digest 0.10.3", ] [[package]] @@ -2657,9 +2586,9 @@ dependencies = [ [[package]] name = "shell-words" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "shlex" @@ -2678,9 +2607,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "smallvec" @@ -2690,9 +2619,9 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "socket2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi", @@ -2719,12 +2648,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - [[package]] name = "strsim" version = "0.10.0" @@ -2824,9 +2747,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.86" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" dependencies = [ "proc-macro2", "quote", @@ -2847,9 +2770,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.0.1" +version = "6.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad3a97fdef3daf935d929b3e97e5a6a680cd4622e40c2941ca0875d6566416f8" +checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709" dependencies = [ "cfg-expr", "heck", @@ -2874,9 +2797,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] @@ -2924,9 +2847,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d54b9298e05179c335de2b9645d061255bcd5155f843b3e328d2cfe0a5b413" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" dependencies = [ "libc", "num_threads", @@ -2949,9 +2872,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" dependencies = [ "bytes", "libc", @@ -2959,9 +2882,10 @@ dependencies = [ "mio", "num_cpus", "once_cell", - "parking_lot", + "parking_lot 0.12.0", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", "winapi", ] @@ -2990,11 +2914,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" +checksum = "4151fda0cf2798550ad0b34bcfc9b9dcc2a9d2471c895c68f3a8818e54f2389e" dependencies = [ - "rustls 0.20.2", + "rustls 0.20.4", "tokio", "webpki 0.22.0", ] @@ -3012,16 +2936,16 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e80b39df6afcc12cdf752398ade96a6b9e99c903dfdc36e53ad10b9c366bca72" +checksum = "06cda1232a49558c46f8a504d5b93101d42c0bf7f911f12a105ba48168f821ae" dependencies = [ "futures-util", "log", - "rustls 0.20.2", + "rustls 0.20.4", "rustls-native-certs 0.6.1", "tokio", - "tokio-rustls 0.23.2", + "tokio-rustls 0.23.3", "tungstenite", "webpki 0.22.0", ] @@ -3040,6 +2964,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" version = "0.5.8" @@ -3057,20 +2995,32 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.29" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" dependencies = [ "cfg-if", "pin-project-lite", + "tracing-attributes", "tracing-core", ] [[package]] -name = "tracing-core" -version = "0.1.21" +name = "tracing-attributes" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90442985ee2f57c9e1b548ee72ae842f4a9a20e3f417cc38dbc5dc684d9bb4ee" dependencies = [ "lazy_static", ] @@ -3083,9 +3033,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "tungstenite" -version = "0.16.0" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1" +checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5" dependencies = [ "base64", "byteorder", @@ -3094,8 +3044,8 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.20.2", - "sha-1 0.9.8", + "rustls 0.20.4", + "sha-1", "thiserror", "url", "utf-8", @@ -3176,9 +3126,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "6.0.1" +version = "6.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467c706f13b7177c8a138858cbd99c774613eb8e0ff42cf592d65a82f59370c8" +checksum = "3893329bee75c101278e0234b646fa72221547d63f97fb66ac112a0569acd110" dependencies = [ "anyhow", "cfg-if", @@ -3230,10 +3180,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] -name = "wasm-bindgen" -version = "0.2.79" +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3241,9 +3197,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.79" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" dependencies = [ "bumpalo", "lazy_static", @@ -3256,9 +3212,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.79" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3266,9 +3222,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.79" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" dependencies = [ "proc-macro2", "quote", @@ -3279,15 +3235,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.79" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" [[package]] name = "web-sys" -version = "0.3.56" +version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" dependencies = [ "js-sys", "wasm-bindgen", @@ -3344,6 +3300,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + [[package]] name = "zerocopy" version = "0.6.1" @@ -3376,9 +3375,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e8f13fef10b63c06356d65d416b070798ddabcadc10d3ece0c5be9b3c7eddb" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" dependencies = [ "proc-macro2", "quote", From a7a260be1656ce1bc8a5e00e38a99d27ca63e400 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 7 Apr 2022 23:20:49 +0200 Subject: [PATCH 141/147] Fix SDL deprecations --- playback/src/audio_backend/sdl.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 1c9794a2..4e390262 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -95,24 +95,24 @@ impl Sink for SdlSink { let samples = packet .samples() .map_err(|e| SinkError::OnWrite(e.to_string()))?; - match self { + let result = match self { Self::F32(queue) => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); drain_sink!(queue, AudioFormat::F32.size()); - queue.queue(samples_f32) + queue.queue_audio(samples_f32) } Self::S32(queue) => { let samples_s32: &[i32] = &converter.f64_to_s32(samples); drain_sink!(queue, AudioFormat::S32.size()); - queue.queue(samples_s32) + queue.queue_audio(samples_s32) } Self::S16(queue) => { let samples_s16: &[i16] = &converter.f64_to_s16(samples); drain_sink!(queue, AudioFormat::S16.size()); - queue.queue(samples_s16) + queue.queue_audio(samples_s16) } }; - Ok(()) + result.map_err(SinkError::OnWrite) } } From 3be6990a13802791d17ee7782f026e881b0ec55d Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 19 Apr 2022 17:29:37 -0500 Subject: [PATCH 142/147] Update dependencies --- Cargo.lock | 119 ++++++++++++----------------- playback/Cargo.toml | 2 +- playback/src/audio_backend/alsa.rs | 5 +- 3 files changed, 52 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd96e5f6..5985f0b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,18 +39,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alsa" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" -dependencies = [ - "alsa-sys", - "bitflags", - "libc", - "nix 0.20.0", -] - [[package]] name = "alsa" version = "0.6.0" @@ -60,7 +48,7 @@ dependencies = [ "alsa-sys", "bitflags", "libc", - "nix 0.23.1", + "nix", ] [[package]] @@ -130,9 +118,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" dependencies = [ "addr2line", "cc", @@ -151,9 +139,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bindgen" -version = "0.56.0" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" +checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" dependencies = [ "bitflags", "cexpr", @@ -224,9 +212,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cexpr" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] @@ -323,9 +311,9 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca4679a59dbd8c15f064c012dfe8c1163b9453224238b59bb9328c142b8b248b" +checksum = "3dff444d80630d7073077d38d40b4501fd518bd2b922c2a55edcc8b0f7be57e6" dependencies = [ "bindgen", ] @@ -336,7 +324,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74117836a5124f3629e4b474eed03e479abaf98988b4bb317e29f08cfe0e4116" dependencies = [ - "alsa 0.6.0", + "alsa", "core-foundation-sys", "coreaudio-rs", "jack", @@ -347,7 +335,7 @@ dependencies = [ "mach", "ndk", "ndk-glue", - "nix 0.23.1", + "nix", "oboe", "parking_lot 0.11.2", "stdweb", @@ -1003,9 +991,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" +checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba" [[package]] name = "httpdate" @@ -1089,7 +1077,7 @@ dependencies = [ "hyper", "log", "rustls 0.20.4", - "rustls-native-certs 0.6.1", + "rustls-native-certs 0.6.2", "tokio", "tokio-rustls 0.23.3", ] @@ -1237,9 +1225,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.122" +version = "0.2.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec647867e2bf0772e28c8bcde4f0d19a9216916e890543b5a03ed8ef27b8f259" +checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50" [[package]] name = "libgit2-sys" @@ -1498,7 +1486,7 @@ dependencies = [ name = "librespot-playback" version = "0.3.1" dependencies = [ - "alsa 0.5.0", + "alsa", "byteorder", "cpal", "futures-util", @@ -1610,13 +1598,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] -name = "miniz_oxide" -version = "0.4.4" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" dependencies = [ "adler", - "autocfg 1.1.0", ] [[package]] @@ -1672,15 +1665,15 @@ dependencies = [ [[package]] name = "ndk-context" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5cc68637e21fe8f077f6a1c9e0b9ca495bb74895226b476310f613325884" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-glue" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9ffb7443daba48349d545028777ca98853b018b4c16624aa01223bc29e078da" +checksum = "0d0c4a7b83860226e6b4183edac21851f05d5a51756e97a1144b7f5a6b63e65f" dependencies = [ "lazy_static", "libc", @@ -1713,18 +1706,6 @@ dependencies = [ "jni-sys", ] -[[package]] -name = "nix" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" -dependencies = [ - "bitflags", - "cc", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.23.1" @@ -1740,12 +1721,12 @@ dependencies = [ [[package]] name = "nom" -version = "5.1.2" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ "memchr", - "version_check", + "minimal-lexical", ] [[package]] @@ -1906,9 +1887,9 @@ dependencies = [ [[package]] name = "object" -version = "0.27.1" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" +checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456" dependencies = [ "memchr", ] @@ -2224,9 +2205,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] @@ -2411,9 +2392,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca9ebdfa27d3fc180e42879037b5338ab1c040c06affd00d8338598e7800943" +checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" dependencies = [ "openssl-probe", "rustls-pemfile", @@ -2423,9 +2404,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "0.2.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" dependencies = [ "base64", ] @@ -2592,9 +2573,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "shlex" -version = "0.1.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "signal-hook-registry" @@ -2943,7 +2924,7 @@ dependencies = [ "futures-util", "log", "rustls 0.20.4", - "rustls-native-certs 0.6.1", + "rustls-native-certs 0.6.2", "tokio", "tokio-rustls 0.23.3", "tungstenite", @@ -2980,9 +2961,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde", ] @@ -2995,9 +2976,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if", "pin-project-lite", @@ -3018,9 +2999,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90442985ee2f57c9e1b548ee72ae842f4a9a20e3f417cc38dbc5dc684d9bb4ee" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" dependencies = [ "lazy_static", ] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index ba51428e..1eee5924 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -28,7 +28,7 @@ tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sy zerocopy = "0.6" # Backends -alsa = { version = "0.5", optional = true } +alsa = { version = "0.6", optional = true } portaudio-rs = { version = "0.3", optional = true } libpulse-binding = { version = "2", optional = true, default-features = false } libpulse-simple-binding = { version = "2", optional = true, default-features = false } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index c639228c..ebeeef13 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -90,11 +90,8 @@ impl From for Format { F32 => Format::float(), S32 => Format::s32(), S24 => Format::s24(), + S24_3 => Format::s24_3(), S16 => Format::s16(), - #[cfg(target_endian = "little")] - S24_3 => Format::S243LE, - #[cfg(target_endian = "big")] - S24_3 => Format::S243BE, } } } From 88f7cdbb44f4d3d1bd10bbdb74963ccb48c9c695 Mon Sep 17 00:00:00 2001 From: eladyn <59307989+eladyn@users.noreply.github.com> Date: Thu, 28 Jul 2022 18:46:16 +0200 Subject: [PATCH 143/147] Fix playlist metadata fields parsing (#1019) Some fields were wrongly parsed as `SpotifyId`s, although they do not always encode exactly 16 bytes in practice. Also, some optional fields caused `[]` to be parsed as `SpotifyId`, which obviously failed as well. --- core/src/cdn_url.rs | 4 ++-- core/src/date.rs | 15 ++++----------- metadata/src/playlist/attribute.rs | 14 +++++++------- metadata/src/playlist/item.rs | 2 +- metadata/src/playlist/list.rs | 24 ++++++++++++++---------- metadata/src/track.rs | 2 +- 6 files changed, 29 insertions(+), 32 deletions(-) diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 7257a9a5..9df43ea9 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -1,5 +1,5 @@ use std::{ - convert::{TryFrom, TryInto}, + convert::TryFrom, ops::{Deref, DerefMut}, }; @@ -152,7 +152,7 @@ impl TryFrom for MaybeExpiringUrls { Ok(MaybeExpiringUrl( cdn_url.to_owned(), - Some(expiry.try_into()?), + Some(Date::from_timestamp_ms(expiry * 1000)?), )) } else { Ok(MaybeExpiringUrl(cdn_url.to_owned(), None)) diff --git a/core/src/date.rs b/core/src/date.rs index 3c78f265..c9aadce8 100644 --- a/core/src/date.rs +++ b/core/src/date.rs @@ -28,12 +28,12 @@ impl Deref for Date { } impl Date { - pub fn as_timestamp(&self) -> i64 { - self.0.unix_timestamp() + pub fn as_timestamp_ms(&self) -> i64 { + (self.0.unix_timestamp_nanos() / 1_000_000) as i64 } - pub fn from_timestamp(timestamp: i64) -> Result { - let date_time = OffsetDateTime::from_unix_timestamp(timestamp)?; + pub fn from_timestamp_ms(timestamp: i64) -> Result { + let date_time = OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128 * 1_000_000)?; Ok(Self(date_time)) } @@ -79,10 +79,3 @@ impl From for Date { Self(datetime) } } - -impl TryFrom for Date { - type Error = crate::Error; - fn try_from(timestamp: i64) -> Result { - Self::from_timestamp(timestamp) - } -} diff --git a/metadata/src/playlist/attribute.rs b/metadata/src/playlist/attribute.rs index eb4fb577..d271bc54 100644 --- a/metadata/src/playlist/attribute.rs +++ b/metadata/src/playlist/attribute.rs @@ -7,7 +7,7 @@ use std::{ use crate::{image::PictureSizes, util::from_repeated_enum}; -use librespot_core::{date::Date, SpotifyId}; +use librespot_core::date::Date; use librespot_protocol as protocol; use protocol::playlist4_external::FormatListAttribute as PlaylistFormatAttributeMessage; @@ -24,7 +24,7 @@ use protocol::playlist4_external::UpdateListAttributes as PlaylistUpdateAttribut pub struct PlaylistAttributes { pub name: String, pub description: String, - pub picture: SpotifyId, + pub picture: Vec, pub is_collaborative: bool, pub pl3_version: String, pub is_deleted_by_owner: bool, @@ -63,7 +63,7 @@ pub struct PlaylistItemAttributes { pub seen_at: Date, pub is_public: bool, pub format_attributes: PlaylistFormatAttribute, - pub item_id: SpotifyId, + pub item_id: Vec, } #[derive(Debug, Clone)] @@ -113,7 +113,7 @@ impl TryFrom<&PlaylistAttributesMessage> for PlaylistAttributes { Ok(Self { name: attributes.get_name().to_owned(), description: attributes.get_description().to_owned(), - picture: attributes.get_picture().try_into()?, + picture: attributes.get_picture().to_owned(), is_collaborative: attributes.get_collaborative(), pl3_version: attributes.get_pl3_version().to_owned(), is_deleted_by_owner: attributes.get_deleted_by_owner(), @@ -146,11 +146,11 @@ impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes { 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()?, + timestamp: Date::from_timestamp_ms(attributes.get_timestamp())?, + seen_at: Date::from_timestamp_ms(attributes.get_seen_at())?, is_public: attributes.get_public(), format_attributes: attributes.get_format_attributes().into(), - item_id: attributes.get_item_id().try_into()?, + item_id: attributes.get_item_id().to_owned(), }) } } diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index dbd5fda2..20f94a0b 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -94,7 +94,7 @@ impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem { revision: item.try_into()?, attributes: item.get_attributes().try_into()?, length: item.get_length(), - timestamp: item.get_timestamp().try_into()?, + timestamp: Date::from_timestamp_ms(item.get_timestamp())?, 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 0a359694..a8ef677b 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -39,12 +39,12 @@ impl Deref for Geoblocks { #[derive(Debug, Clone)] pub struct Playlist { pub id: NamedSpotifyId, - pub revision: SpotifyId, + pub revision: Vec, pub length: i32, pub attributes: PlaylistAttributes, pub contents: PlaylistItemList, - pub diff: PlaylistDiff, - pub sync_result: PlaylistDiff, + pub diff: Option, + pub sync_result: Option, pub resulting_revisions: Playlists, pub has_multiple_heads: bool, pub is_up_to_date: bool, @@ -77,12 +77,12 @@ impl Deref for RootPlaylist { #[derive(Debug, Clone)] pub struct SelectedListContent { - pub revision: SpotifyId, + pub revision: Vec, pub length: i32, pub attributes: PlaylistAttributes, pub contents: PlaylistItemList, - pub diff: PlaylistDiff, - pub sync_result: PlaylistDiff, + pub diff: Option, + pub sync_result: Option, pub resulting_revisions: Playlists, pub has_multiple_heads: bool, pub is_up_to_date: bool, @@ -202,17 +202,21 @@ impl TryFrom<&::Message> for SelectedListContent { type Error = librespot_core::Error; fn try_from(playlist: &::Message) -> Result { Ok(Self { - revision: playlist.get_revision().try_into()?, + revision: playlist.get_revision().to_owned(), 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()?, + diff: playlist.diff.as_ref().map(TryInto::try_into).transpose()?, + sync_result: playlist + .sync_result + .as_ref() + .map(TryInto::try_into) + .transpose()?, 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()?, + timestamp: Date::from_timestamp_ms(playlist.get_timestamp())?, owner_username: playlist.get_owner_username().to_owned(), has_abuse_reporting: playlist.get_abuse_reporting_enabled(), capabilities: playlist.get_capabilities().into(), diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 001590f5..4ab9b2b4 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -132,7 +132,7 @@ impl TryFrom<&::Message> for Track { sale_periods: track.get_sale_period().try_into()?, previews: track.get_preview().into(), tags: track.get_tags().to_vec(), - earliest_live_timestamp: track.get_earliest_live_timestamp().try_into()?, + earliest_live_timestamp: Date::from_timestamp_ms(track.get_earliest_live_timestamp())?, has_lyrics: track.get_has_lyrics(), availability: track.get_availability().try_into()?, licensor: Uuid::from_slice(track.get_licensor().get_uuid()) From 5e60e75282ae48794fc1b0a226287afe9a0a77c2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 28 Jul 2022 18:48:26 +0200 Subject: [PATCH 144/147] Add lockfile --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9327ea1..9c56f131 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1897,9 +1897,9 @@ dependencies = [ [[package]] name = "ogg" -version = "0.9.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960d0efc0531a452c442c777288f704b300a5f743c04a14eba71f9aabc4897ac" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" dependencies = [ "byteorder", ] From 4ec38ca1937f90a2c683d17e0eb8c22ffea6fc79 Mon Sep 17 00:00:00 2001 From: "Art M. Gallagher" Date: Thu, 28 Jul 2022 11:25:41 +0100 Subject: [PATCH 145/147] related project: snapcast (#1023) include a mention in the README of the snapcast project that uses librespot for Spotify sources --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5dbb5487..793ade7e 100644 --- a/README.md +++ b/README.md @@ -117,3 +117,4 @@ functionality. - [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. +- [Snapcast](https://github.com/badaix/snapcast) - synchronised multi-room audio player that uses librespot as its source for Spotify content From 6b11fb5ceef07120d834fcb66265b24769729a98 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 28 Jul 2022 19:06:38 +0200 Subject: [PATCH 146/147] Update MSRV to 1.60 --- .github/workflows/test.yml | 6 +++--- COMPILING.md | 14 +++++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7503b764..54366092 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.56 # MSRV (Minimum supported rust version) + - "1.60" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -113,7 +113,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - 1.56 # MSRV (Minimum supported rust version) + - "1.60" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -160,7 +160,7 @@ jobs: os: [ubuntu-latest] target: [armv7-unknown-linux-gnueabihf] toolchain: - - 1.56 # MSRV (Minimum supported rust version) + - "1.60" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/COMPILING.md b/COMPILING.md index 5176e906..ec1d174e 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -7,11 +7,7 @@ In order to compile librespot, you will first need to set up a suitable Rust bui ### Install Rust 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. -<<<<<<< HEAD -*Note: The current minimum required Rust version at the time of writing is 1.56.* -======= -*Note: The current minimum required Rust version at the time of writing is 1.56, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* ->>>>>>> dev +*Note: The current minimum supported Rust version at the time of writing is 1.60.* #### Additional Rust tools - `rustfmt` 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: @@ -22,8 +18,8 @@ rustup component add clippy Using `cargo fmt` and `cargo clippy` is not optional, as our CI checks against this repo's rules. ### General dependencies -Along with Rust, you will also require a C compiler. - +Along with Rust, you will also require a C compiler. + On Debian/Ubuntu, install with: ```shell sudo apt-get install build-essential @@ -31,10 +27,10 @@ sudo apt-get install build-essential ``` On Fedora systems, install with: ```shell -sudo dnf install gcc +sudo dnf install gcc ``` ### Audio library dependencies -Depending on the chosen backend, specific development libraries are required. +Depending on the chosen backend, specific development libraries are required. *_Note this is an non-exhaustive list, open a PR to add to it!_* From 9e06b11609cad6fe1987f6e5a4647c58b823e291 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 28 Jul 2022 19:32:11 +0200 Subject: [PATCH 147/147] Update MSRV to 1.61 and fix test --- .github/workflows/test.yml | 6 +++--- COMPILING.md | 2 +- core/tests/connect.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 54366092..caa2722a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.60" # MSRV (Minimum supported rust version) + - "1.61" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -113,7 +113,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.60" # MSRV (Minimum supported rust version) + - "1.61" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -160,7 +160,7 @@ jobs: os: [ubuntu-latest] target: [armv7-unknown-linux-gnueabihf] toolchain: - - "1.60" # MSRV (Minimum supported rust version) + - "1.61" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/COMPILING.md b/COMPILING.md index ec1d174e..527be464 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -7,7 +7,7 @@ In order to compile librespot, you will first need to set up a suitable Rust bui ### Install Rust 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 supported Rust version at the time of writing is 1.60.* +*Note: The current minimum supported Rust version at the time of writing is 1.61.* #### Additional Rust tools - `rustfmt` 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: diff --git a/core/tests/connect.rs b/core/tests/connect.rs index 9411bc87..91679f91 100644 --- a/core/tests/connect.rs +++ b/core/tests/connect.rs @@ -7,8 +7,8 @@ use librespot_core::{authentication::Credentials, config::SessionConfig, session #[tokio::test] async fn test_connection() { timeout(Duration::from_secs(30), async { - let result = Session::new(SessionConfig::default(), None, false) - .connect(Credentials::with_password("test", "test")) + let result = Session::new(SessionConfig::default(), None) + .connect(Credentials::with_password("test", "test"), false) .await; match result {